SNDGM — генератор звуковых эффектов на Game Maker Language

SNDGM

Разработка начата мной там, где-то в марте 2014. В планах — сделать генератор звуковых эффектов, который может делать примерно то же, что и широко известный в узких кругах (Ludum Dare, скажем) sfxr, об использовании которого у меня был довольно объёмный пост, и ещё другие вещи, которые сюда слишком долго писать, а в релизе они всё равно будут. Вдохновлён следующими аудио-редакторами и звуковыми манипуляторами: GoldWave, sfxr/bfxr/as3fxr, Caustic (машины Modular и 8-Bit Synth), встроенный редактор семплов ModPlugTracker (позже известный как OpenMPT), ну и моими собственными старыми аудио-поделками.

SNDGM 0.1 — генерирует 1 секунду звука по формуле, за 4 секунды. В формулу могут входить все встроенные функции, доступные в GML, а также функция bitcrush, реализованная соответствующим скриптом внутри исходного кода, которая делает из 8-битного звука 2-битный, привнося в звучание больше «квадратности». Кнопка WAV делает вывод во внешний файл SNDGM.wav.

SNDGM 0.2 — всё то же самое, только теперь за пол-секунды, или даже меньше. Добавлена функция smooth, которая возвращает новое значение, равное 0.5*предыдущее+0.5*текущее, то есть смягчает звук. Я не знаю точно, что из спектра частот при этом теряется, но видимо что-то сверху, да и какая разница. Внешний вид не изменился.

SNDGM 0.3 — более простой синтаксис для простейших импульсов, новые функции.

Процессоры:

bitcrush: понижает разрядность 8-битного сигнала до 2 бит (то есть 4 позиций: 0, 64, 128, 192).

smooth: сглаживает сигнал, домешивая 50% от предыдущего значения к 50% текущего.

mix: микширует два входных значения в одно, 50:50.

mix3: то же самое для трёх значений, коэффициент 0.333.

carry: преобразует значения из [-1, 1] в [0, 254] (-1 соответствует 0, 1 — 254).

Импульсы:

sine — синусоида, cosine — косинусоида

saw — пилообразный

square — квадратный

Начиная с этой версии можно использовать в формулах два новых параметра, помимо t, которые являются тем же самым t, только отмасштабированным для нужд синусоид (s) и для нужд пилообразных импульсов (b) соответственно.К сожалению, для тех, кто просто хочет генерировать звуки для игр, дело это тёмное, и пишу я это скорее для себя, и какого-то узкого круга заинтересованных лиц. Но всё же поясню подробней.

Раньше, как видно в формуле, заданной в SNDGM 0.2 изначально, синусоиду можно было получать так: 127+sin(t/n)*127, где n это делитель частоты, который никак толком не соотносился с частотой результирующей волны. Сегодня я перестроил генератор так, чтобы можно было получить синусоиду во-первых чётко заданной частоты, а во-вторых всего лишь введя sine(f), где f это нужная частота. Каким образом это было достигнуто?

  1. Организация внятного параметра для получения требуемой частоты: Введён отмасштабированный параметр s, вычисленный от t из того условия, что за одну секунду звука он должен проходить все фазы синусоиды (ну или косинусоиды). Технически там вычисляется ds — параметр итератора, равный degtorad(360/22050), где 360 это градусы, а 22050 это частота дискретизации, но это уже программерская сторона вопроса. После этого шага стало возможно получать синусоиду нужной частоты f, по формуле 127+sin(s*f)*127;
  2. Избавление от громоздкой конструкции 127+(…)*127: Создан скрипт для того, чтобы делать из вещественного сигнала от -1.0 до +1.0 сигнал от 0 до 255 (технически вышло до 254, но ~0.4% потери сигнала это не страшно), то есть, в частности, преобразовывать результат синусоиды в результат, который можно нормально услышать. Скрипт этот называется carry, и доступен для использования в формулах наравне с другими функциями GM и моими скриптами. Правда, во время написания этой строки, я задумался, зачем называть его carry, если он не работает с несущими частотами, да и вообще ни с частотами, ни с несущими, а преобразует знаковое значение в интервале [-1, 1] в беззнаковое в интервале [0, 254], но в SNDGM 0.3 название остаётся таким. После этого шага формула из предыдущего шага могла быть заменена уже на carry(sin(s*f));
  3. Запрятывание всей этой математической мишуры туда, где она должна быть — под капот: Создан скрипт sine, который возвращает значение carry(sin(s*argument[0])). Всё. sine(f) получает синусоиду частоты f. Для полноты картины я добавил ещё и cosine, так что cosine(f) тоже работает.

Это всё было про параметр s. А на кой факториал нам нужен b? Как я сказал в самом начале, он нужен для пилообразных импульсов заданной частоты, не зависящей от каких-то ещё внешних параметров. Отцепив у t зависимости от 8-битности и от частоты дискретизации в 22050Гц, получаем удельное db=256/22050. db это здесь не дециБелы, тем более что они обозначаются дБ, а delta b. Само b у меня означает byte, и, по-моему, история с carry повторяется, потому что название-то от фонаря, но пусть тогда будет bit, и типа это отсылается на олдскул, чиптюн, и прочую ностальгию. Кстати, 2-битный звук от моего bitcrush не так ужасно звучит, как я думал. Я забыл, где был 2-битный звук, но помню, что щёлкал в каком-то трекере, и трещало оно довольно сильно. А разгадка одна — частота дискретизации-то была, наверное, 8КГц, а не 22. А то и все 4КГц.

Короче, импульсы теперь генерируются так: sine(f), square(f), saw(f). И это хорошо.

SNDGM 0.4 — умеет генерировать треугольный импульс и пересобирать WAV-файл заново при его отсутствии (RIFF-контейнер и данные о формате: беззнаковый 8-битный 22050Гц PCM), поэтому начиная с этой версии я не включаю в архив дистрибутива файл «SNDGM.wav». Добавил кнопку для пост-эффектов, которые применяются на уже сгенерированном звуке, и два пост-эффекта. ober добавляет обертон, если я правильно понял. smooth уже был описан выше, и описан в списке функций ниже. Также новая функция-процессор clip берёт из сигнала среднюю часть (не по спектру, а по значениям колебаний).

Процессоры:

bitcrush: понижает разрядность 8-битного сигнала до 2 бит (то есть 4 позиций: 0, 64, 128, 192).

smooth: сглаживает сигнал, домешивая 50% от предыдущего значения к 50% текущего.

mix: микширует два входных значения в одно, 50:50.

mix3: то же самое для трёх значений, коэффициент 0.333.

ober: добавляет в звук обертон (умножает входное значение на его долю от максимально возможного значения).

clip: «отрезает» все значения выше и ниже указанных границ.

Импульсы:

sine — синусоида, cosine — косинусоида

saw — пилообразный

square — квадратный

triangle — треугольный

Конвертеры:

carry: преобразует значения из [-1, 1] в [0, 254] (-1 соответствует 0, 1 — 254).

bi: [0, 1] в [-1, 1]

uni: [-1, 1] в [0, 1]

SNDGM 0.5 — умеет генерировать прямоугольный импульс функцией rectangle(f, n), где f это частота, а n это параметр скважности, который может принимать значения между 0 и 256, правда при 0 и 256 он будет генерировать тишину, так что полезный интервал это [1, 255]. rectangle(220, b) позволяет получить традиционный PWM (Pulse Width Modulation). Вызов вида rectangle(f) будет равносилен rectangle(f, 0) и поэтому тоже даст тишину.

Также умеет генерировать шум — белый, и не очень. noise(n) выдаёт на выход результат псевдослучайного генератора вида irandom(255), и применяет к нему уже описанный эффект smooth n раз.

Ещё добавлено вот это, после попытки создать несуществовавший файл:

if file_exists(«SNDGM.wav»)==false
{
show_message(«Файл SNDGM.wav не удалось создать. Возможно, файловая система переполнена или защищена от записи.»)

exit
}

Добавлен параметр «Базовая частота», который задаётся извне, и может быть использован в формуле, будучи туда введённым в виде f. Иными словами, звук, похожий на звук органа, который я получил позавчера, теперь можно генерировать не этой формулой:

mix(mix3(sine(55), sine(110), sine(220)), mix3(sine(440), sine(880), sine(1760)))

, а вот этой:

mix(mix3(sine(f), sine(f*2), sine(f*4)), mix3(sine(f*8), sine(f*16), sine(f*32)))

Только теперь параметр базовой частоты можно свободно менять, и таким образом отрендерить все ноты гипотетического органа без пересчитывания и подставления всех кратных частот вручную. Не то чтобы это кому-то было нужно в век VST-плагинов, которые всё генерируют на лету, но подождите, история только начинается.

Также добавлена нормализация в пост-эффекты:

minimum=256
maximum=-1
for (t=0 t<o_wave.length t+=1)
{
minimum=min(minimum, o_wave.value[t])
maximum=max(maximum, o_wave.value[t])
}

if (minimum==0 && maximum==255)
|| (minimum==maximum)
break;

k=255/(maximum-minimum)

for (t=0 t<o_wave.length t+=1)
{
o_wave.value[t]=(o_wave.value[t]-minimum)*k
}

Благодаря новому микшеру mixn генерировать пресловутый орган теперь ещё проще:

mixn(6, sine(f), sine(f*2), sine(f*4), sine(f*8), sine(f*16), sine(f*32))

Импульсы:

sine — синусоида, cosine — косинусоида

saw — пилообразный

square — квадратный

triangle — треугольный

rectangle(f, n) — прямоугольный с частотой f и скважностью n=[1, 255]

noise(n) — белый шум, интерполированный n раз. При n=0 является белым, далее становится более мягким.

Процессоры:

bitcrush(signal, n): понижает разрядность 8-битного сигнала до 1+n бит (то есть при n=0 либо не указанном (bitcrush(signal) — до 2 позиций: 0, 128). При n>6 уничтожает сигнал полностью.

smooth: сглаживает (интерполирует) сигнал, домешивая к 50% каждого значения 50% от предыдущего значения.

mix: микширует два входных значения в одно, 50:50.

mix3: то же самое для трёх значений, коэффициент 0.333.

mixn: микширует столько значений, сколько указано в первом параметре, но не более 15. Например mixn(5, sine(f/4), sine(f/2), sine(f), sine(f*2), sine(f*4)) смешивает 5 указанных параметров.

ober: добавляет в звук обертон (умножает входное значение на его долю от максимально возможного значения).

clip(низ, сигнал, верх): «отрезает» все значения выше и ниже указанных границ.

Конвертеры:

carry: преобразует значения из [-1, 1] в [0, 254] (-1 соответствует 0, 1 — 254).

bi: [0, 1] в [-1, 1]

uni: [-1, 1] в [0, 1]

В планах

  • Оптимизировать отрисовку, 22050 линий вместо 640 — это не дело; Да я же это сделал ещё в 0.2;
  • Разработать интерфейс для настройки ADSR-огибающей (или она таки не нужна?);
  • Сделать больше скриптов для обработки генерируемого сигнала (пополняю регулярно, начиная с 0.2).
  • Написать внятную инструкцию, включаемую в дистрибутив программы. Ведь оказывается, визуализацию волны можно прокручивать влево и вправо!
  • Добавить треугольный импульс и PWM-импульс (то есть квадратный со скважностью, он же прямоугольный).
  • Добавить что-то шумовое, чтобы быть поближе к разнообразию в sfxr.
  • Лишить пользователя возможности затупить с WAV-файлом, генерируя новый с нуля, если текущий не подходит, или что-то в таком духе.

Дальше, при портировании на GM8.1

  • Сделать прослушивание звука сразу после генерации;
  • Расширить максимально доступную длину звука до, скажем, 10 секунд за счёт встроенных структур данных, а именно ds_list;
  • Сделать больше параметров при вычислении, помимо времени (параметры b и s это всё равно параметры времени, только сжатые). Кроме формулы надо как-то регулировать сам алгоритм генерации. Подумать над FM-модуляцией.
  • Сделать, что ли, 16 бит и 44КГц? Если не будет лень.