Пишем простой осциллятор




 

Я надеюсь, что вы прочитали главу "Обзор архитектуры синтезатора Syntage" — я буду объяснять все в терминах своей архитектуры.

 

Самый простой звук — это чистый тон (синусоидальный сигнал, синус) определенной частоты. В природе вы вряд ли сможете услышать чистый тон. В жизни же можно услышать чистые тона в какой-нибудь электронике (и то, уверенности мало). Фурье сказал, что любой звук можно представить как одновременное звучание тонов разной частоты и громкости. Окраска звука характеризуется тембром — грубо говоря, описанием соотношения тонов в этом звуке (спектром).

 

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

 

Какие выбрать "простые" сигналы? Очевидно, сигналы, спектр которых известен и хорошо изучен, которые легко обрабатывать. Возьмем четыре знаменитые типа сигналов:

 

 

Периоды четырех типов сигналов: синус, треугольник, импульс/квадрат, пила.

 

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

 

Синус имеет глухое и тихое звучание, остальные же — "острое" и громкое. Это связано с тем, что, в отличие от синуса, другие сигналы содержат большое количество других тонов (гармоник) в спектре.

 

Наш генерируемый сигнал будет характеризоваться двумя параметрами: типом волны и частотой.
На графике изображены периоды нужных нам волн. Заметьте, что все волны представлены в интервале от 0 до 1. Это очень удобно, так как позволяет одинаково запрограммировать расчет значений. Такой подход позволяет задать произвольную форму сигнала, я даже видел синтезаторы, где можно вручную его нарисовать.

 

По представленным картинкам напишем вспомогательный класс WaveGenerator, с методом GetTableSample, который будет возвращать значение амплитуды сигнала в зависимости от типа волны и времени (время должно быть в пределах от 0 до 1).

 

Добавим так же в тип волны белый шум — он полезен в синтезе нестандартных звуков. Белый шум характеризуется тем, что спектральные составляющие равномерно распределены по всему диапазону частот. Функция NextDouble стандартного класса Random имеет равномерное распределение — таким образом, мы можем считать, что каждый сгенерированный семпл относится к некоторой гармонике. Соответственно, мы будем выбирать гармоники равномерно, получая белый шум. Нужно лишь сделать отображение результата функции из интервала [0,1] в интервал минимального и максимального значения амплитуды [-1,1].

 

public static class WaveGenerator{ public enum EOscillatorType { Sine, Triangle, Square, Saw, Noise } private static readonly Random _random = new Random(); public static double GetTableSample(EOscillatorType oscillatorType, double t) { switch (oscillatorType) { case EOscillatorType.Sine: return Math.Sin(DSPFunctions.Pi2 * t); case EOscillatorType.Triangle: if (t < 0.25) return 4 * t; if (t < 0.75) return 2 - 4 * t; return 4 * (t - 1); case EOscillatorType.Square: return (t < 0.5f)? 1: -1; case EOscillatorType.Saw: return 2 * t - 1; case EOscillatorType.Noise: return _random.NextDouble() * 2 - 1; default: throw new ArgumentOutOfRangeException(); } }}

 

Теперь, пишем класс Oscillator, который будет наследником SyntageAudioProcessorComponentWithParameters<AudioProcessor>. В осцилляторе рождается звук, поэтому класс будет реализовывать интерфейс IGenerator, а именно функцию

 

IAudioStream Generate();

 

Необходимо запросить у IAudioStreamProvider (для нас это будет родительский AudioProcessor) аудиопоток, и в каждом вызове функции Generate заполнять его сгенерированными семплами.

 

Пока что у нашего осциллятора будет два параметра:

 

· Тип волны — WaveGenerator.EOscillatorType, используем класс EnumParameter из Syntage.Framework

· Частота сигнала — слышимый диапазон от 20 до 20000 Гц, используем класс FrequencyParameter из Syntage.Framework

 

Оформим все вышесказанное:

 

public class Oscillator: SyntageAudioProcessorComponentWithParameters<AudioProcessor>, IGenerator{ private readonly IAudioStream _stream; // поток, куда будем генерировать семплы private double _time; public EnumParameter<WaveGenerator.EOscillatorType> OscillatorType { get; private set; } public RealParameter Frequency { get; private set; } public Oscillator(AudioProcessor audioProcessor): base(audioProcessor) { _stream = Processor.CreateAudioStream(); // запрашиваем поток } public override IEnumerable<Parameter> CreateParameters(string parameterPrefix) { OscillatorType = new EnumParameter<WaveGenerator.EOscillatorType>(parameterPrefix + "Osc", "Oscillator Type", "Osc", false); Frequency = new FrequencyParameter(parameterPrefix + "Frq", "Oscillator Frequency", "Hz"); return new List<Parameter> { OscillatorType, Frequency }; } public IAudioStream Generate() { _stream.Clear(); // очищаем все, что было раньше GenerateToneToStream(); // самое интересное return _stream; }}

 

Осталось написать функцию GenerateToneToStream.

 

Каждый раз когда мы будем генерировать семплы сигнала, мы должны помнить о двух значениях:

 

· длина текущего буфера

· частота дискретизации

 

Оба параметра могут меняться во время работы плагина, поэтому не советую каким-либо образом их кешировать. Каждый вызов функции Generate() на вход плагину подается буфер конечной длины (длина определяется хостом, по времени она достаточно короткая) — звук генерируется "порциями". Мы должны запоминать, сколько времени прошло с момента начала генерирования волны, чтобы звук был "непрерывным". Пока что звук будем генерировать с момента старта плагина. Синхронизировать звук с нажатием клавиши будем в следующей статье.

 

Семплы генерируются в цикле от 0 до [длина текущего буфера].

 

Частота дискретизации — число семплов в секунду. Время, которое проходит от начала одного семпла до другого равно timeDelta = 1/SampleRate. При частоте дискретизации 44100 Гц это очень маленькое время — 0.00002267573 секунды.

 

Теперь мы можем знать, сколько времени в секундах прошло с момента старта до текущего семпла — заведем переменную _time и будем прибавлять к ней timeDelta каждую итерацию цикла.

 

Чтобы воспользоваться функцией WaveGenerator.GetTableSample нужно знать относительное время от 0 до 1, где 1 — период волны. Зная нужную частоты волны, мы знаем и ее период — значение, обратное частоте.

 

Нужное относительное время мы можем получить как дробную часть деления прошедшего времени на период волны.

 

Пример: мы генерируем синус со знаменитой частотой 440 Гц. Из частоты находим период синуса: 1/440 = 0.00227272727 секунды.
Частота дискретизации 44100 Гц.
Рассчитаем 44150-й семпл, если на нулевом семпле время равнялось нулю.
На 44150-м семпле прошло 44150/44100 = 1.00113378685 секунд.
Смотрим, сколько это в периодах — 1.00113378685/0.00227272727 = 440.498866743.
Отбрасываем целую часть — 0.498866743. Именно это значение и нужно передать в функцию WaveGenerator.GetTableSample.

 

Если записать все символьно, получим:

 

 

Оформим выкладки в виде отдельной функции WaveGenerator.GenerateNextSample и запишем итоговую функцию GenerateToneToStream.

 

public static double GenerateNextSample(EOscillatorType oscillatorType, double frequency, double time){ var ph = time * frequency; ph -= (int)ph; // реализация frac вычитанием целой части return GetTableSample(oscillatorType, ph);}...private void GenerateToneToStream(){ var count = Processor.CurrentStreamLenght; // сколько семплов нужо сгенерировать double timeDelta = 1.0 / Processor.SampleRate; // столько времени разделяет два соседних семпла // кешируем ссылки на каналы, чтобы было меньше обращений в цикле var leftChannel = _stream.Channels[0]; var rightChannel = _stream.Channels[1]; for (int i = 0; i < count; ++i) { // Frequency и OscillatorType лучше не кешировать - это параметры плагина и // они могут меняться var frequency = DSPFunctions.GetNoteFrequency(Frequency.Value); var sample = WaveGenerator.GenerateNextSample(OscillatorType.Value, frequency, _time); leftChannel.Samples[i] = sample; rightChannel.Samples[i] = sample; _time += timeDelta; }}

 

Обычно, в параметры осциллятора добавляют следующие:

 

· Громкость

· Подстройка (Fine) — изменение частоты генерируемой волны в большую или меньшую сторону. Можно получить эффект, похожий на wah-wah если модулировать этот параметр. Если генераторов много и они смешиваются, можно делать расстройку генераторов друг относительно друга.

· Панировка/Стерео (Pan/Panning/Stereo) отношение громкостей сигнала в левом и правом ухе.

 

Данные параметры есть в реализованном мною синтезаторе — вы можете самостоятельно их реализовать.

 

Осталось реализовать классы AudioProcessor (будет создавать осциллятор и вызывать у него метод Generate) и PluginController (создает AudioProcessor).
Посмотрите реализацию данных классов в моем коде Syntage. На текущем этапе AudioProcessor нужен, чтобы:

 

· Создать осциллятор

· Заполнить параметры (вызвать функцию CreateParameters)

· В функции обработки буфера семплов вызывать метод Generte у осциллятора

 

Простая реализация перечисленных классов, для ленивых

 

В следующей статье я расскажу как написать ADSR-огибающую.

 

Удачи в программировании!

 

P.S. В заголовке я писал что занимаюсь музыкой — если кому то интересно, можете послушать мою музыку, и в частности записанный diy-альбом.

 


Список литературы

 

1. Теория звука. Что нужно знать о звуке, чтобы с ним работать. Опыт Яндекс.Музыки.

2. Марпл-мл. С. Л. Цифровой спектральный анализ и его приложения.

3. Айфичер Э., Джервис Б. — Цифровая обработка сигналов. Практический подход.

4. Martin Finke's Blog "Music & Programming" цикл статей по созданию синта от и до на C++, используя библиотеку WDL-OL.

5. Хабр-переводы Martin Finke's Blog

6. Модульные аналоговые синтезаторы (большая хабр-статья затрагивающая вопросы синтеза звука, обзора аналоговых синтезаторов и их составляющих).

Теги:

  • .NET
  • С#
  • VST
  • VSTi
  • VST.NET
  • DSP
  • Цифровая обработка сигналов
  • синтезатор
  • синтез звука

Реклама

ЧИТАЮТ СЕЙЧАС

· Проекты, которые не взлетели

K26

· Чем проще задача, тем чаще я ошибаюсь

K10

· Придумываем технологию Powercheck

K4

· Гигантская пирамида PlusToken собрала 1% существующих биткоинов и схлопнулась

K16

· Алкоголизм последней стадии

K536

· «За нами следят»: что может происходить в неприметном минивэне прямо у вас под окном

K27

· +53

· 195

· 25,1k

· 19

 

44,0

Карма

 

0,0

Рейтинг

 

Подписчики

 

Подписки

 

Иван Ларцов lis355

Землянин

Поделиться публикацией

·

·

·

·

·

ПОХОЖИЕ ПУБЛИКАЦИИ

· 19 октября 2016 в 04:40

Программирование&Музыка: Частотный фильтр Баттервота. Часть 3

+2811,5k10411

· 5 октября 2016 в 04:06

Программирование&Музыка: ADSR-огибающая сигнала. Часть 2

+237,1k7310

· 14 января 2011 в 00:01



Поделиться:




Поиск по сайту

©2015-2024 poisk-ru.ru
Все права принадлежать их авторам. Данный сайт не претендует на авторства, а предоставляет бесплатное использование.
Дата создания страницы: 2019-12-19 Нарушение авторских прав и Нарушение персональных данных


Поиск по сайту: