
В прошлой статье мы рассмотрели, как создать простой синтезатор на основе Raspberry Pi Pico. Звуки на основе синусоиды интересны, но хочется получить звук реального инструмента с его уникальным тембром.
Если вы думаете, что достаточно просто проигрывать записанный звук для каждой ноты, то, по крайней мере, для Raspberry Pi такой подход не сработает из-за ограниченных ресурсов. А вот использование звуковых шрифтов (SoundFont), которые придумали ещё в прошлом веке, поможет решить задачу.
Изобретать велосипед и реализовывать обработку SoundFont-файлов с нуля мы не будем, а воспользуемся готовой библиотекой TinySoundFont.
Изучение технологии SoundFont и то, как устроены звуковые банки вообще, даёт много в понимании синтеза звука.
В этой статье мы подключим библиотеку TinySoundFont к Raspberry Pi Pico, устраним главный недостаток библиотеки — загрузку всего SoundFont в оперативную память, и заставим синтезатор читать семплы напрямую из флеш-памяти.
Хотя сейчас используются и более продвинутые технологии вроде VST, для нашего Raspberry Pi Pico они не годятся, так как ему просто не хватит производительности.
Что же мы слышим как музыкальный инструмент?
Задумывались ли вы, почему мы различаем звук скрипки от звука фортепиано, даже если на них играют одни и те же ноты?
В идеальном мире каждая нота — это гармонические колебания определённой частоты.
В реальном мире звук музыкального инструмента состоит из основного тона и набора кратных тону обертонов, которые вместе образуют уникальный тембр.
Обертоны (гармоники): колебания с частотами, которые в 2, 3, 4 и более раз выше основной частоты ноты.
Атака и затухание: громкость звука меняется со временем — в момент удара или смычка возникает шум и всплеск высоких частот, которые затем затухают.
Нелинейность и биения: реальные материалы струн или воздуха слегка несовершенны, поэтому частоты гармоник могут чуть сдвигаться, создавая «живое» звучание.
Тембр: именно набор и сила обертонов позволяют нам отличить скрипку от пианино, даже если они играют абсолютно ту же самую ноту.
Фаза и модуляция: частота колебаний в реальности никогда не стоит на месте на 100%, частота постоянно микро-колеблется (вибрато), что делает звук приятным для уха.
Звуковые банки
Наиболее распространёнными стандартизованными форматами банков инструментов являются DLS (Downloadable Sounds) и SF2 (SoundFont 2). Формат DLS был стандартизован MIDI Manufacturers Association и применялся в операционных системах, прежде всего в Windows, тогда как SF2 получил широкое распространение в программных синтезаторах благодаря простоте использования и богатой экосистеме готовых звуковых банков. В Windows звуковой банк находится в файле C:\Windows\System32\drivers\gm.dls. Но на использование его в своих программах накладываются лицензионные ограничения.
Много звуковых банков хранится на сайте Polyphone.io.
Банки можно редактировать и преобразовывать. Например, можно создать банк, который содержит только нужные инструменты, что значительно сокращает размер звукового банка. Такой минимальный банк можно поместить во флеш-памяти Raspberry Pi Pico.
Я использовал онлайн-редактор для просмотра звуковых банков на сайте Spessasus.

Можно применить Soundfont к MIDI-файлу и получить оцифрованный звук. Это увлекательное занятие, которое может занять у вас несколько часов. В Linux это удобно сделать через программу audacious. Она входит в дистрибутивы Linux и в Debian устанавливается командой:
sudo apt install audacious
Мы будем использовать формат Sound Font 2
Как MIDI-нота превращается в звук при использовании SoundFont
Превращение MIDI-ноты в звук с помощью SoundFont (.sf2) происходит через выбор команды ноты, поиск нужного аудио-семпла в базе данных и настройку семпла по высоте и громкости. Весь этот процесс выполняет программный или аппаратный семплерный синтезатор.

Шаг 1: Приём MIDI-сообщения
Нота вошла (Note On): Синтезатор получает команду, где указан номер ноты (высота от 0 до 127) и сила нажатия — Velocity (громкость от 0 до 127).
Номер инструмента (Program Change): Сообщение указывает, какой именно музыкальный инструмент (например, пианино, гитара или скрипка) нужно использовать.
Шаг 2: Поиск данных в SoundFont (.sf2).
Пресет и инструмент: Файл SoundFont устроен в виде дерева. Команда инструмента обращается к нужному пресету внутри файла.
Зоны и семплы: Банк ищет аудиозапись (PCM-семпл), которая подходит по высоте и силе нажатия (Velocity). Для разных зон клавиатуры могут использоваться разные записи живого инструмента.
Шаг 3: Обработка и генерация звука
Изменение высоты (Pitch Shift): Если исходный семпл записан для ноты «Ля» первой октавы, а пришла нота «Си», синтезатор ускоряет или замедляет воспроизведение аудиофайла, чтобы получить нужную частоту.
Громкость и огибающая (Envelope): Параметр Velocity определяет начальную громкость, а фильтры и генераторы из файла .sf2 задают форму звука (как звук нарастает, звучит и затухает после отпускания клавиши (Note Off)).
Микширование: Полученный цифровой аудиопоток объединяется с другими нотами и превращается в звук на динамиках.
Файл SoundFont (.sf2)
Файлы с расширением .sf2 — это специальные звуковые банки, которые хранят не только аудиосемплы музыкальных инструментов, но и правила их использования. Они связывают записанные звуки с MIDI-командами, позволяя синтезатору определять, какой семпл необходимо воспроизвести и с какими параметрами.
В sf2-файле хранятся:
Аудиосемплы (Samples). Записанные в формате WAV звуки музыкальных инструментов, синтезированных тембров, ударных или других источников. Обычно для одного инструмента используется несколько семплов, записанных для разных диапазонов нот.
Инструменты (Instruments). Наборы семплов, объединённые в один логический инструмент. Для каждого семпла задаётся диапазон MIDI-нот и дополнительные параметры воспроизведения, благодаря чему синтезатор выбирает нужный семпл.
Пресеты (Presets). Готовые тембры, которые выбираются через MIDI-сообщение Program Change. Пресет может состоять из одного или нескольких инструментов, объединённых в единую программу.
Параметры настройки (Generators и Modulators). Generators задают постоянные параметры воспроизведения: громкость, высоту тона, огибающую (envelope), фильтр и другие характеристики. Modulators описывают, как эти параметры изменяются в зависимости от силы нажатия клавиши (velocity), положения колеса модуляции, Pitch Bend и других MIDI-событий.
Как правило, семплерный синтез звука опирается на стандартную систему музыкального строя (где нота Ля первой октавы имеет частоту 440 Гц).
У каждого записанного звука есть корневая нота (исходный питч семпла). Зная её, движок определяет, с какой скоростью нужно проиграть аудиофайл, чтобы получить правильное звучание остальных нот на клавиатуре.
Чтобы инструмент звучал естественно, в SF2-файлах клавиатуру делят на регионы (диапазоны), назначая на каждый свой семпл со своей корневой нотой. Для финальной ювелирной настройки для каждого семпла задаётся питч-коррекция в центах (где 100 центов — это полутон, а 1200 центов — полная октава).
Для ещё более естественного воспроизведения звуков инструмента используются ADSR-огибающие, а также модуляция высоты тона, громкости (амплитуды) и фильтра низких частот при помощи низкочастотных осцилляторов (LFO).
Частотная модуляция: LFO плавно меняет высоту тона (Pitch) семпла, создавая эффект вибрато.
Амплитудная модуляция: LFO меняет громкость (Volume) семпла, создавая эффект тремоло.
Фильтровая модуляция: LFO управляет частотой среза (Cutoff), создавая ритмичную пульсацию или эффект «вау-вау».
В SoundFont 2 LFO используют треугольную форму волны, потому что она обеспечивает плавную двунаправленную модуляцию без резких скачков.
На слабых процессорах прошлого — очень просто генерируется с помощью счётчика, который поочерёдно считает вверх и вниз. Форма волны для LFO принимается треугольной, это объясняется тем, что её легко было получить при помощи простого счётчика на маломощных процессорах прошлого.
Теперь, когда мы ознакомились, как устроен SoundFont, давайте рассмотрим, как работать с библиотекой TinySoundFont.
Библиотека TinySoundFont
Библиотека TinySoundFont позволяет рендерить цифровой звук из MIDI-потока в хорошем качестве, но у неё есть небольшой недостаток: без небольших правок она не будет работать на платах типа Raspberry Pi Pico, так как ей не хватит оперативной памяти.
Библиотека копирует все семплы из флеш-памяти в оперативную. Но у RP2040 и RP2350 есть возможность работать с флеш-памятью как с оперативной (правда, только на чтение).
Подключим библиотеку TinySoundFont к проекту на Pico. Сама библиотека — это два заголовочных файла, которые можно скопировать в директорию, но сделаем это более правильно, через команду git.
Теперь нужно найти звуковой банк, который будет использовать библиотека. Размещать мы его будем во флеш-памяти Raspberry Pi Pico, поэтому важен размер файла. У оригинальных Raspberry Pi Pico флеш-память 2МБ для Raspberry Pi Pico и 4 МБ для Raspberry Pi Pico 2 и до 16 МБ для различных китайских клонов и других плат на базе RP2040 и RP2350.
Библиотека не полностью подходит для Raspberry Pi Pico, так как загружает весь SoundFont-файл в оперативную память. Оперативной памяти Raspberry Pico будет недостаточно, поэтому я расскажу, как пропатчил библиотеку TinySoundFont, чтобы библиотека работала с флеш-памятью, а не оперативной.
Также TinySoundFont использует вычисления с плавающей точкой, поэтому без значительных модификаций библиотеки добиться звучания без треска у меня не получилось. Необходима Raspberry Pi Pico 2 c RP2350, где такая возможность есть.
Если вам хочется, чтобы Pico звучал как легендарный синтезатор Roland SC-55, то лучше воспользоваться клоном Raspberry Pi Pico 2 c RP2350 и 16 мегабайт флеш-памяти.
Патчинг библиотеки TinySoundFont
Библиотека TinySoundFont делает две нежелательные вещи при загрузке семплов: загружает семплы в оперативную память, а также переводит их в вещественный тип. Для минимизации изменений мы оставим всю вещественную логику библиотеки, но в инструкциях по интерполированию будем выполнять нормализацию к вещественным числам на лету.
Весь код для синтеза звука находится в файле tsf.h.
-
Модифицируем структуру
tsf_stream, добавляем поле:unsigned int (*get_pos)(void* data);
Оно необходимо, чтобы можно получить текующу позицию в потоке из программы.
В структуре
tsfменяем тип поляfontSamplesсfloat*наshort*. fontSamples у нас берутся из soundFont файла, как есть, без преобразования воfloat.-
Добавляем реализацию функции для получения текущей позиции в потоке:
static unsigned int tsf_stream_memory_get_pos(struct tsf_stream_memory* m) { return m->pos;} -
Меняем инициализацию переменной
streamc:struct tsf_stream stream = { TSF_NULL, (int(*)(void*,void*,unsigned int))&tsf_stream_memory_read, (int(*)(void*,unsigned int))&tsf_stream_memory_skip };на
struct tsf_stream stream = { TSF_NULL, (int(*)(void*,void*,unsigned int))&tsf_stream_memory_read, (int(*)(void*,unsigned int))&tsf_stream_memory_skip, (unsigned int (*) (void*))&tsf_stream_memory_get_pos };Вот где я вспомнил своего преподавателя по С, и мои знания интерпретации составных описателей пригодились.
-
Меняем сигнатуру функции
tsf_load_samplesc:static int tsf_load_samples(void** pRawBuffer, float** pFloatBuffer, unsigned int* pSmplCount, struct tsf_riffchunk *chunkSmpl, struct tsf_stream* stream)на:
static int tsf_load_samples(void** pRawBuffer, short** pShortBuffer, unsigned int* pSmplCount, struct tsf_riffchunk *chunkSmpl, struct tsf_stream* stream) -
Удаляем из функции код, который отвечает о выделении динамической памяти и преобразование типа sample points с
shortво float.*pFloatBuffer = (float*)TSF_MALLOC(*pSmplCount * sizeof(float)); if (!*pFloatBuffer || !stream->read(stream->data, *pFloatBuffer, chunkSmpl->size)) return 0; for (res = *pFloatBuffer, out = res + *pSmplCount, in = (short*)res + *pSmplCount; out != res;) *(--out) = (float)(*(--in) / 32767.0); В функции
tsf_voice_renderменяет тип переменной intput сfloat*наshort*-
Также в этой функции меняем код для интерполирования семплов с
float alpha = (float)(tmpSourceSamplePosition - pos), val = (input[pos] * (1.0f - alpha) + input[nextPos] * alpha);на:
float alpha = (float)(tmpSourceSamplePosition - pos), val = (input[pos] / 32767.0f * (1.0f - alpha) + input[nextPos] / 32767.0f * alpha);Да, это неэффективно, так как мы выполняем одно и то же деление множество раз, но по-другому нельзя, так как мы читаем напрямую из флеш-памяти, а оперативной у нас очень мало.
В функции
tsf_loadменяем переменнуюfloat* floatBufferнаshort* shortBuffer.-
В этой же функции в случае загрузки чанка
smplдобавляем код для инициализацииshortBuffer.const char * tmp = ((struct tsf_stream_memory*)stream->data)->buffer; unsigned int pos = ((struct tsf_stream_memory*)stream->data)->pos; shortBuffer = (short*)(tmp + pos); stream->skip(stream->data, chunk.size);
Проблемы, с которыми столкнулся
Из-за того, что не хватает производительности чипа RP2040 (программа не успевает рассчитать звук, перед заполнением аудиобуфера) мы слышим треск. Я думаю, дело в том, что RP2040 не имеет аппаратной поддержки работы с вещественными числами.
Увеличение тактовой частоты привело к тому, что треск практически исчез, но полифония не работала, при нажатии нескольких клавиш на MIDI явно слышался не аккорд.
Можно было попробовать прооптимизировать работу с вещественными числами, убрать часть логики по фильтрации, но у меня был Raspberry Pi Pico 2, и полифония заработала на нём без треска даже без разгона чипа. Но желание улучшить библиотеку для RP2040 осталось, и, возможно, в будущем я её попробую сделать.
Ну а теперь, когда наш синтезатор играет почти как настоящий инструмент, можно разбираться углубляться в фотмат sf2 и синтез музыки на основе семплов, и что на низком уровне делает библиотека Tiny Sound File.
Заключение
Нам удалось успешно запустить библиотеку TinySoundFont на Raspberry Pi Pico благодаря небольшому патчу, который позволяет читать семплы напрямую из флеш-памяти, не загружая весь банк в оперативную память. На плате с чипом RP2350 (Pico 2) синтезатор работает стабильно, с приемлемой полифонией и без заметного треска даже без разгона. На классическом RP2040 ситуация сложнее: отсутствие аппаратной поддержки вычислений с плавающей точкой приводит к тому, что процессор не успевает рассчитывать звук в реальном времени, особенно при нескольких одновременно звучащих нотах.
Вопрос оптимизации TinySoundFont под RP2040 остаётся открытым. Возможные направления — переход на фиксированную арифметику в критических участках, упрощение или отключение части фильтров и модуляторов, а также более агрессивное использование DMA и PIO. Если удастся добиться чистого звука на обычном Pico, это значительно расширит круг доступных платформ для подобных проектов.
Помимо практического результата, работа со SoundFont и семплерным синтезом даёт хорошее понимание того, как устроен цифровой звук: от структуры банков и ADSR-огибающих до работы LFO и интерполяции семплов. Это отличный способ глубже погрузиться в обработку цифровых сигналов (DSP) на микроконтроллерах с очень ограниченными ресурсами.
© 2026 ООО «МТ ФИНАНС»
avitek
Можно было бы посоветовать перевести вычисления на фиксированную точку (читай - целочисленные), но у Cortex-M0 из команд умножения есть только с 32-битным результатом.
А вот M33 (rp2350) уже умеет в умножение с 64-битным накоплением (smlal/umlal), там можно развернуться. Но придётся погружаться внутрь синтеза, и наверное глубоко...