18в. Синтезатор на STM32: разбор модулей Mutable Instruments — кодек, блок сэмплов и DSP в реальном времени
В разделах 18а и 18б звук шёл через I2S и SAI: кадр, частота отсчётов, двойной буфер DMA. В мини-проекте 198п был свой простой синтезатор на ЦАП. Здесь — как те же приёмы собраны в настоящих приборах: модулях формата Eurorack фирмы Mutable Instruments. Их исходники и схемы открыты, модули выпускались серийно, и в коде видно всё: какой кодек, какой блок отсчётов, как напряжение управления превращается в ноту и как генератор обходится без наложения спектра.
Разбор идёт по пути сигнала: вход управления → АЦП → калибровка → расчёт блока → DMA → I2S → кодек. Код генератора и фильтра модуля Braids собран на компьютере и проверен опытом: точность строя, АЧХ фильтра, спектр пилы с полиномиальным BLEP и без него.
Опоры: проект eurorack Mutable Instruments (github.com/pichenettes/eurorack,
коммит 08460a6, автор — Émilie Gillet): makefile модулей,
драйверы */drivers/codec.cc, plaits/drivers/audio_dac.cc, plaits/ui.cc,
plaits/settings.cc, braids/braids.cc, braids/analog_oscillator.cc,
braids/svf.h, генератор таблиц braids/resources/lookup_tables.py и схемы
*/hardware_design/pcb/*.sch. Лицензия: код для STM32 — MIT, схемы и
платы — CC-BY-SA 3.0 (README проекта); цитаты ниже — с этой пометкой.
Регистры STM32F405 — по заголовку stm32f405xx.h и HAL F4 (пак STM32F4xx_DFP 2.17.1). Кроме разбора —
свой маленький голос синтезатора на C: вход V/OCT с калибровкой, пила с
polyBLEP, фильтр и блок для DMA, плюс настройка I2S и DMA на F405 по
регистрам — целиком, для повторения. Про фильтры вообще — раздел 81а,
про точную частоту I2S — 18а.
Как это называется в документации
Оригинал Перевод control voltage (CV) напряжение управления 1 V/octave (V/OCT) один вольт на октаву: +1 В — нота на октаву выше gate, trigger уровень «нажато» и короткий импульс запуска codec, audio DAC кодек (АЦП + ЦАП звука), звуковой ЦАП block size размер блока: сколько отсчётов считается за один вызов half transfer / transfer complete прерывания DMA «половина» и «конец» буфера phase increment шаг фазы за отсчёт band-limited step (BLEP), polyBLEP поправка к скачку сигнала против наложения спектра aliasing наложение спектра: гармоники выше половины частоты отсчётов «отражаются» вниз state variable filter (SVF) фильтр с переменными состояния: ФНЧ, полосовой и ФВЧ сразу damping, resonance затухание и резонанс фильтра; добротность Q = 1 / затухание lookup table (LUT) таблица заранее посчитанных значений
18в.1 Модули и микроконтроллеры
Eurorack — формат модульных синтезаторов: каждый модуль — отдельная панель в общем корпусе, модули соединяются шнурами. По шнурам идут звук и напряжения управления: высота ноты (1 В на октаву), огибающие, уровни «нажато» (gate) и импульсы запуска (trigger). Питание — общая двуполярная шина корпуса; точных напряжений шины в файлах проекта нет, поэтому здесь они не называются.
Какой модуль на каком микроконтроллере, видно по FAMILY и F_CPU
в makefile и по обозначениям на схемах:
| Модуль | Что делает | МК на схеме | F_CPU |
Выход звука |
|---|---|---|---|---|
| Braids | генератор | STM32F103CBT6 | 72 МГц | ЦАП DAC8551 по SPI |
| Plaits | генератор | STM32F373CCT6 | 72 МГц | PCM5100 по I2S |
| Clouds | гранулярная обработка | STM32F405RGT6 | 168 МГц | WM8731 по I2S |
| Rings | резонатор | STM32F405RGT6 | 168 МГц | WM8731 по I2S |
| Elements | модальный синтез | STM32F405RGT6 | 168 МГц | WM8731 по I2S |
| Warps | модулятор | STM32F405RGT6 | 168 МГц | WM8731 по I2S |
Tides, Peaks, Frames, Streams и Yarns — тоже STM32F103 (FAMILY = f10x),
Stages и Tides 2 — STM32F373 (f37x), Marbles — F405 (f4xx).
Раскладка простая. Где нужен звуковой вход (Clouds, Rings, Elements,
Warps обрабатывают внешний сигнал) — F405 и кодек WM8731: АЦП и ЦАП в
одном корпусе. Где звук только выходит — F373 и ЦАП PCM5100 или F103 и
ЦАП DAC8551. У F373 для входов управления есть 16-битные сигма-дельта АЦП
(SDADC), ими и читается V/OCT в Plaits.
Одного этого репозитория для сборки мало. Каждый makefile кончается
строкой include stmlib/makefile.inc, а stmlib в проекте — подмодуль
git, и в архиве репозитория его папка пуста. Поэтому ниже код Braids собран
по частям — с короткой заменой тех функций stmlib, без которых он не
компилируется (об этом — в 18в.4).
18в.2 Кодек, I2S и двойной буфер DMA
F405 и WM8731. Все четыре модуля на F405 подключают кодек одинаково:
настройка по I2C2 (адрес 0x1A), звук — по I2S2, причём передача идёт
через SPI2, а приём — через дополнительный модуль I2S2ext, как в 18а.
Передачу ведёт DMA1 Stream4, приём — DMA1 Stream3, оба по кругу,
полусловами. Прерывание берётся только у приёмного потока — и по нему
считаются обе половины:
void DMA1_Stream3_IRQHandler(void) {
if (AUDIO_I2S_EXT_DMA_REG->AUDIO_I2S_EXT_DMA_ISR & AUDIO_I2S_EXT_DMA_FLAG_TC) {
AUDIO_I2S_EXT_DMA_REG->AUDIO_I2S_EXT_DMA_IFCR = AUDIO_I2S_EXT_DMA_FLAG_TC;
rings::Codec::GetInstance()->Fill(1);
}
if (AUDIO_I2S_EXT_DMA_REG->AUDIO_I2S_EXT_DMA_ISR & AUDIO_I2S_EXT_DMA_FLAG_HT) {
AUDIO_I2S_EXT_DMA_REG->AUDIO_I2S_EXT_DMA_IFCR = AUDIO_I2S_EXT_DMA_FLAG_HT;
rings::Codec::GetInstance()->Fill(0);
}
}
(rings/drivers/codec.cc, MIT.) Флаги читаются прямо из регистров:
по комментарию автора — чтобы не тратить вызовы функций библиотеки.
Fill(0) считает первую половину, пока DMA передаёт вторую, Fill(1) —
наоборот. Длина буфера — 2 * stride * block_size * 2 полуслов: два
канала, две половины. У Rings блок 24 кадра, при ведущем STM32
(stride = 1) это 96 полуслов.
stride — второй режим. В исправленной версии платы ведущим становится
кодек, и тогда, как написано в коде, данные идут «с набивкой»: при 48 кГц
каждый второй слот пустой (stride = 2), при 32 кГц — два из трёх.
Fill до расчёта сдвигает отсчёты вплотную, после — раздвигает обратно.
Частота. Такт I2S даёт отдельная ФАПЧ PLLI2S. Числа N и R в драйвере подобраны «для кварца 8 МГц»; если вход ФАПЧ 1 МГц, делитель I2S (с выходом MCLK, fs = I2SCLK / (256 · делитель)) даёт то же, что написано в комментариях:
| Частота | N / R | I2SCLK, Гц | Делитель | Получается, Гц | Ошибка | Комментарий в коде |
|---|---|---|---|---|---|---|
| 48 000 | 258 / 3 | 86 000 000 | 7 | 47 991,07 | −0,0186 % | 47.992kHz |
| 44 100 | 271 / 6 | 45 166 666 | 4 | 44 108,07 | +0,0183 % | 44.11kHz |
| 32 000 | 426 / 4 | 106 500 000 | 13 | 32 001,20 | +0,0038 % | 32.003kHz |
| 96 000 | 393 / 4 | 98 250 000 | 4 | 95 947,27 | −0,0549 % | 95.95 kHz |
48 кГц с ошибкой −0,0186 % — тот же предел, что в 18а и 18б. Худший случай — 96 кГц у Warps: −0,95 цента, на слух не заметно.
То же своими руками. Ниже — настройка звука F405 по регистрам, без библиотек: ФАПЧ на 86 МГц, I2S2 — ведущий передатчик с MCLK, I2S2ext — ведомый приёмник, два потока DMA по кругу и прерывание, которое считает свободную половину. Выводы, потоки и каналы DMA — те же, что в драйвере Rings. Настройка самого кодека по I2C сюда не входит.
/* Раздел 18в: звук на STM32F405 по регистрам — PLLI2S, I2S2 (передача) и I2S2ext (приём),
* DMA1 по кругу с двумя половинами, расчёт блока в прерывании. Выводы и потоки DMA —
* как у кодека WM8731 в модулях Mutable Instruments: PB12 WS, PB13 CK, PB15 SD, PB14 ext_SD,
* PC6 MCK, все AF5; передача — DMA1 Stream4 канал 0, приём — Stream3 канал 3.
* Настройка самого кодека по I2C здесь не повторяется. Вход ФАПЧ — 1 МГц. */
#include "stm32f4xx.h"
#include "synth.h"
#define BLOCK 24u /* кадров в половине буфера */
#define PLLI2S_N 258u /* 1 МГц · 258 / 3 = 86 МГц */
#define PLLI2S_R 3u
#define I2SPR_48K (3u | SPI_I2SPR_ODD | SPI_I2SPR_MCKOE) /* 86 МГц / (256 · 7) = 47 991 Гц */
#define I2S_MASTER_TX (SPI_I2SCFGR_I2SMOD | SPI_I2SCFGR_I2SCFG_1) /* Philips, 16 бит */
#define I2S_SLAVE_RX (SPI_I2SCFGR_I2SMOD | SPI_I2SCFGR_I2SCFG_0)
#define DMA_TX_CR ((0u << DMA_SxCR_CHSEL_Pos) | DMA_SxCR_PL_1 | DMA_SxCR_MSIZE_0 | DMA_SxCR_PSIZE_0 \
| DMA_SxCR_MINC | DMA_SxCR_CIRC | DMA_SxCR_DIR_0)
#define DMA_RX_CR ((3u << DMA_SxCR_CHSEL_Pos) | DMA_SxCR_PL_1 | DMA_SxCR_MSIZE_0 | DMA_SxCR_PSIZE_0 \
| DMA_SxCR_MINC | DMA_SxCR_CIRC | DMA_SxCR_HTIE | DMA_SxCR_TCIE)
_Static_assert(I2SPR_48K == 0x0303u, "I2SDIV 3, ODD, MCKOE");
_Static_assert(I2S_MASTER_TX == 0x0A00u && I2S_SLAVE_RX == 0x0900u, "I2SCFGR");
_Static_assert(DMA_TX_CR == 0x00022D40u && DMA_RX_CR == 0x06022D18u, "DMA_SxCR");
static int16_t tx[2u * BLOCK * 2u]; /* 2 канала × 2 половины */
static int16_t rx[2u * BLOCK * 2u];
static synth_t voice;
static cal_t cal = { -60.0f, 25.71f }; /* значения по умолчанию, как в Plaits */
volatile int16_t voct_code; /* код АЦП входа V/OCT, пишет драйвер АЦП */
static void pins_af5(void)
{
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN | RCC_AHB1ENR_GPIOCEN;
GPIOB->MODER = (GPIOB->MODER & ~0xFF000000u) | 0xAA000000u; /* PB12…PB15 — альтернативная функция */
GPIOB->AFR[1] = (GPIOB->AFR[1] & ~0xFFFF0000u) | 0x55550000u;
GPIOB->OSPEEDR |= 0xFF000000u;
GPIOC->MODER = (GPIOC->MODER & ~(3u << 12)) | (2u << 12); /* PC6 — MCK */
GPIOC->AFR[0] = (GPIOC->AFR[0] & ~(0xFu << 24)) | (5u << 24);
GPIOC->OSPEEDR |= 3u << 12;
}
static void plli2s_86mhz(void)
{
RCC->CR &= ~RCC_CR_PLLI2SON;
RCC->PLLI2SCFGR = (PLLI2S_N << RCC_PLLI2SCFGR_PLLI2SN_Pos) | (PLLI2S_R << RCC_PLLI2SCFGR_PLLI2SR_Pos);
RCC->CFGR &= ~RCC_CFGR_I2SSRC; /* такт I2S — от PLLI2S */
RCC->CR |= RCC_CR_PLLI2SON;
while (!(RCC->CR & RCC_CR_PLLI2SRDY)) { }
}
void audio_start(void)
{
synth_init(&voice, 47991.07f);
synth_set(&voice, 48.0f, 84.0f, 3.0f);
pins_af5();
plli2s_86mhz();
RCC->APB1ENR |= RCC_APB1ENR_SPI2EN;
RCC->AHB1ENR |= RCC_AHB1ENR_DMA1EN;
SPI2->I2SPR = I2SPR_48K;
SPI2->I2SCFGR = I2S_MASTER_TX;
I2S2ext->I2SCFGR = I2S_SLAVE_RX;
SPI2->CR2 = SPI_CR2_TXDMAEN;
I2S2ext->CR2 = SPI_CR2_RXDMAEN;
DMA1_Stream4->PAR = (uint32_t)&SPI2->DR; /* передача: память -> SPI2 */
DMA1_Stream4->M0AR = (uint32_t)tx;
DMA1_Stream4->NDTR = 2u * BLOCK * 2u;
DMA1_Stream4->CR = DMA_TX_CR;
DMA1_Stream3->PAR = (uint32_t)&I2S2ext->DR; /* приём: I2S2ext -> память */
DMA1_Stream3->M0AR = (uint32_t)rx;
DMA1_Stream3->NDTR = 2u * BLOCK * 2u;
DMA1_Stream3->CR = DMA_RX_CR;
NVIC_EnableIRQ(DMA1_Stream3_IRQn);
DMA1_Stream3->CR |= DMA_SxCR_EN;
DMA1_Stream4->CR |= DMA_SxCR_EN;
I2S2ext->I2SCFGR |= SPI_I2SCFGR_I2SE; /* ведомый — первым */
SPI2->I2SCFGR |= SPI_I2SCFGR_I2SE;
}
/* Половина half свободна: DMA сейчас передаёт другую. */
static void fill(unsigned half)
{
synth_set(&voice, voct_to_note(voct_code, &cal) + 36.0f, 84.0f, 3.0f);
synth_render(&voice, &tx[half * BLOCK * 2u], BLOCK);
}
void DMA1_Stream3_IRQHandler(void)
{
uint32_t isr = DMA1->LISR;
if (isr & DMA_LISR_HTIF3) {
DMA1->LIFCR = DMA_LIFCR_CHTIF3;
fill(0);
}
if (isr & DMA_LISR_TCIF3) {
DMA1->LIFCR = DMA_LIFCR_CTCIF3;
fill(1);
}
}
_Static_assert сверяют числа с полями заголовка: I2SPR = 0x0303
(I2SDIV = 3, ODD, MCKOE — делитель 7), I2SCFGR — 0x0A00 и 0x0900,
DMA_SxCR — 0x00022D40 и 0x06022D18. arm-none-eabi-gcc собрал файл для
Cortex-M4F без предупреждений: 492 байта кода, из них audio_start —
336, прерывание — 156; буферы и голос — 414 байт ОЗУ. На плате файл
не запускался.
F373 и PCM5100. У F373 нет звуковой ФАПЧ: такт бита — просто делитель
системной частоты. В plaits/dsp/dsp.h (MIT) автор посчитал это сам:
// Bit clock = 72000000 / 47 = 1531.91 kHz
// Frame clock = Bit clock / 32 = 47872.34 Hz
//
// That's only 4.6 cts of error, but we care!
static const float kCorrectedSampleRate = 47872.34f;
Расчёт делителя по правилу библиотеки ST для 16 бит без MCLK даёт те же 47,
такт бита 1 531 914,89 Гц и частоту 47 872,34 Гц — на 4,61 цента ниже
48 кГц. Исправлять такт не стали: вместо этого все частоты нот в Plaits
считаются от kCorrectedSampleRate, и строй остаётся точным. Кодек здесь
не нужен вовсе: PCM5100 — только ЦАП, DMA1 Channel5 кормит SPI2 в
режиме передачи, прерывания «половина» и «конец» зовут Fill(1) и
Fill(0), буфер — 2 * block_size * 2 = 48 полуслов при блоке 12.
F103 и DAC8551. В Braids нет ни I2S, ни DMA для звука: таймер TIM1
с периодом F_CPU / 96000 − 1 = 749 вызывает прерывание 96 000 раз
в секунду, и оно пишет один отсчёт в ЦАП по SPI2. Отсчёты лежат в кольце
из четырёх блоков по 24 (audio_samples[kNumBlocks][kBlockSize]), основной
цикл досчитывает следующий блок, пока прерывание играет текущий. Тот же
двойной буфер, только «DMA» сделан программой.
18в.3 Вход V/OCT: ОУ, АЦП и калибровка
Напряжение управления нельзя подать на вывод STM32 напрямую: оно бывает
отрицательным и больше 3,3 В. В Plaits его приводит инвертирующий
сумматор на ОУ; в коде калибровки (plaits/ui.cc, MIT) автор записал его
формулу в комментариях:
void Ui::CalibrateC3() {
// (-33/100.0*1 + -33/140.0 * -10.0) / 3.3 * 2.0 - 1 = 0.228
float c1 = cv_c1_;
// (-33/100.0*1 + -33/140.0 * -10.0) / 3.3 * 2.0 - 1 = -0.171
float c3 = pitch_lp_calibration_;
float delta = c3 - c1;
if (delta > -0.6f && delta < -0.2f) {
ChannelCalibrationData* c = settings_->mutable_calibration_data(
CV_ADC_CHANNEL_V_OCT);
c->scale = 24.0f / delta;
c->offset = 12.0f - c->scale * c1;
settings_->SavePersistentData();
mode_ = UI_MODE_NORMAL;
} else {
mode_ = UI_MODE_ERROR;
}
normalization_probe_.Init();
}
Читается так: вход через 100 кОм, обратная связь 33 кОм (усиление −0,33),
через 140 кОм подмешана опора −10 В (на схеме — источник LM4040B10),
результат в пределах 0…3,3 В переводится в число x от −1 до +1
(float_value делит 16-битный код на 32 768). Во второй строке
комментария напряжение то же «1» — описка: для 3 В получается как раз
−0,171. Программа посчитала формулу:
- x = 0,429 − 0,2 · V: наклон 0,200 на вольт, x = +1 при −2,857 В, x = −1 при 7,143 В — вход охватывает ровно 10 В;
- при 1 В x = 0,229, при 3 В — −0,171, разность −0,400;
scale = 24 / delta= −60,00 полутона на единицу x,offset = 12 − scale · c1= 25,714.
Это ровно значения по умолчанию из plaits/settings.cc: scale = -60.0f,
offset = 25.71. 1 В на октаву в коде виден напрямую: 1 В → нота 12,
3 В → нота 36, два вольта — 24 полутона.
Калибровка. Владелец подаёт 1 В и 3 В, модуль запоминает показания
(CalibrateC1, CalibrateC3) и пишет в память scale и offset для V/OCT;
для остальных входов по тем же шагам снимается только смещение. Проверка
delta > -0.6f && delta < -0.2f отбрасывает явную ошибку — например,
перепутанные напряжения. Хранятся данные в PersistentData через модуль
хранения во флеш из подмодуля stmlib (здесь не разбирается).
То же в своём коде (synth.c):
/* 16-битный код АЦП -> x от -1 до +1, как float_value() в Plaits */
float voct_x(int16_t code)
{
return (float)code / 32768.0f;
}
/* Калибровка по двум точкам: 1 В -> нота 12, 3 В -> нота 36 (24 полутона на 2 В).
* Возвращает 0 и ничего не меняет, если разность не похожа на правду. */
int cal_two_points(cal_t *c, float x_1v, float x_3v)
{
float delta = x_3v - x_1v;
if (!(delta > -0.6f && delta < -0.2f))
return 0;
c->scale = 24.0f / delta;
c->offset = 12.0f - c->scale * x_1v;
return 1;
}
float voct_to_note(int16_t code, const cal_t *c)
{
return voct_x(code) * c->scale + c->offset;
}
float note_to_hz(float note)
{
return 440.0f * exp2f((note - 69.0f) / 12.0f);
}
Опыт на компьютере: коды АЦП для 1 В и 3 В взяты из той же модели входа —
7490 и −5617. Калибровка дала scale = −60,0009, offset = 25,7148;
перепутанные 1 В и 3 В отвергнуты. На 0…5 В с шагом 1 мВ нота отличается
от 12 · V не больше чем на 0,193 цента — это округление кода АЦП до
16 бит.
Зачем калибровать, видно по числам. Если вход не 100 кОм, а 101 кОм (допуск 1 %), наклон меняется, и без калибровки строй уходит на −11,88 цента на октаву — −59,4 цента на пяти октавах. Шаг 16-битного кода — 60 / 32 768 полутона = 0,183 цента; это на порядки меньше.
18в.4 Генератор: одна октава таблицы и точность строя
В Braids высота — целое число в 1/128 полутона (pitch_ = 60 << 7 — нота
60), фаза — 32 бита, частота отсчётов 96 кГц. Шаг фазы берётся из таблицы
lut_oscillator_increments, посчитанной заранее lookup_tables.py — в ней
97 чисел на одну октаву выше всего диапазона, шаг таблицы 1/8 полутона.
Ноты ниже получаются сдвигом вправо — делением на 2 за октаву:
uint32_t AnalogOscillator::ComputePhaseIncrement(int16_t midi_pitch) {
if (midi_pitch >= kHighestNote) {
midi_pitch = kHighestNote - 1;
}
int32_t ref_pitch = midi_pitch;
ref_pitch -= kPitchTableStart;
size_t num_shifts = 0;
while (ref_pitch < 0) {
ref_pitch += kOctave;
++num_shifts;
}
uint32_t a = lut_oscillator_increments[ref_pitch >> 4];
uint32_t b = lut_oscillator_increments[(ref_pitch >> 4) + 1];
uint32_t phase_increment = a + \
(static_cast<int32_t>(b - a) * (ref_pitch & 0xf) >> 4);
phase_increment >>= num_shifts;
return phase_increment;
}
(braids/analog_oscillator.cc, MIT.) Программа пересчитала таблицу по
формуле из lookup_tables.py — совпали все 97 чисел. Таблицы фильтра
(lut_svf_cutoff, lut_svf_damp, по 257 чисел) отличаются от формулы не
больше чем на единицу — это округление.
Как собран опыт. Файлы analog_oscillator.cc, svf.h и нужные таблицы
из resources.cc скопированы без изменений. Вместо пустого stmlib —
замена на три десятка строк: целые типы, макросы CLIP и
DISALLOW_COPY_AND_ASSIGN и функции таблиц Interpolate824,
Interpolate88, Crossfade. Пилу и ComputePhaseIncrement замена не
затрагивает; фильтр берёт из неё только линейную интерполяцию по таблице.
g++ собрал всё без предупреждений.
Точность строя. Программа перебрала все высоты с шагом 1/128 полутона и сравнила частоту из шага фазы (шаг · 96 000 / 2³²) с точной 440 · 2^((n − 69)/12):
- на пяти октавах, от ноты 36 (65,41 Гц) до 96 (2093 Гц), и на всём диапазоне 0…127 наибольшая ошибка — +0,0113 цента (между точками таблицы — там работает линейная интерполяция);
- на целых нотах 36, 48, 60, 72, 84, 96 — не больше 0,0002 цента.
Одной октавы в таблице хватает: сдвиг теряет младшие биты шага, но шаг велик — у ноты 36 это 2 926 232.
18в.5 Пила без наложения спектра: полиномиальный BLEP
Пила «в лоб» — старшие биты фазы — на каждом периоде резко падает. Резкий скачок содержит гармоники до бесконечности, и всё, что выше 48 кГц, отражается вниз, в слышимую полосу, нестройным шумом. Braids сглаживает скачок: в двух соседних отсчётах вокруг сброса фазы к сигналу прибавляется поправка — квадратичный полином от того, где между отсчётами был скачок:
inline int32_t ThisBlepSample(uint32_t t) {
if (t > 65535) {
t = 65535;
}
return t * t >> 18;
}
inline int32_t NextBlepSample(uint32_t t) {
if (t > 65535) {
t = 65535;
}
t = 65535 - t;
return -static_cast<int32_t>(t * t >> 18);
}
(braids/analog_oscillator.h, MIT.) t — доля отсчёта, прошедшая после
переполнения фазы, в 16 битах: phase_ / (phase_increment >> 16).
Опыт. Программа отрендерила настоящим RenderSaw по 65 536 отсчётов
блоками по 24 и рядом — ту же фазу без поправки. Спектр — окно
Блэкмана — Харриса; «гармоники» — полосы вокруг k · f0 ниже 48 кГц, всё
прочее — наложение:
| Нота, частота | Гармоник до 48 кГц | Наложение к гармоникам: без / с BLEP | Худшая побочная до 20 кГц: без / с BLEP |
|---|---|---|---|
| 96, 2093,0045 Гц | 22 | −15,6 / −31,1 дБ | −31,8 / −58,0 дБ |
| 84, 1046,5022 Гц | 45 | −18,7 / −34,5 дБ | −36,7 / −61,6 дБ |
Поправка в две строки убирает около 15 дБ мощности наложения и 26 дБ у самой заметной побочной составляющей. Плата — завал верха: у ноты 96 последняя, 22-я гармоника без поправки −26,8 дБ (как у идеальной пилы, 1/k), с поправкой −34,0 дБ; у ноты 84 45-я — −33,2 и −40,7 дБ.
Свой вариант в float — классический polyBLEP: та же идея, поправка — квадратичный полином от доли отсчёта, фаза — от 0 до 1:
/* Поправка к скачку пилы: t — фаза 0…1, dt — шаг фазы за отсчёт */
float polyblep(float t, float dt)
{
if (t < dt) { /* отсчёт сразу после скачка */
t /= dt;
return t + t - t * t - 1.0f;
}
if (t > 1.0f - dt) { /* отсчёт перед скачком */
t = (t - 1.0f) / dt;
return t * t + t + t + 1.0f;
}
return 0.0f;
}
float synth_saw(synth_t *s)
{
float y = 2.0f * s->phase - 1.0f - polyblep(s->phase, s->inc);
s->phase += s->inc;
if (s->phase >= 1.0f)
s->phase -= 1.0f;
return y;
}
float synth_saw_naive(synth_t *s)
{
float y = 2.0f * s->phase - 1.0f;
s->phase += s->inc;
if (s->phase >= 1.0f)
s->phase -= 1.0f;
return y;
}
При 48 кГц у ноты 96 (2093,0044 Гц) ниже 24 кГц всего 11 гармоник, и наложение сильнее, чем у Braids на 96 кГц: без поправки −12,5 дБ к мощности гармоник и −22,5 дБ у худшей побочной; с поправкой — −28,4 и −34,9 дБ. Та же плата: 11-я гармоника −27,6 дБ вместо −20,5 дБ.
arm-none-eabi-g++ (-mcpu=cortex-m3 -Os) собрал RenderSaw в 322 байта
кода, ComputePhaseIncrement — в 64, весь analog_oscillator.cc — в
2954 байта; таблицы, которые он использует, — 11 692 байта.
18в.6 Фильтр SVF в целых числах и срок блока
Фильтр Braids (braids/svf.h, MIT) — классическая схема с переменными
состояния в 16-битной арифметике: ФНЧ, полосовой и ФВЧ считаются одним
проходом.
int32_t notch = in - (bp_ * damp >> 15);
lp_ += f * bp_ >> 15;
CLIP(lp_)
int32_t hp = notch - lp_;
bp_ += f * hp >> 15;
CLIP(bp_)
Частота среза задаётся нотой (как высота генератора), коэффициент f
берётся из таблицы lut_svf_cutoff = 2 · sin(π · fc / 96 000), затухание —
из lut_svf_damp. Выше fs / 8 = 12 000 Гц коэффициент не растёт: первая
«упёртая» нота — 127 (12 543,9 Гц).
Опыт. Программа подавала синус и перебирала частоту шагом 1/48 октавы,
у пика — 1/384 октавы (3,1 цента). Добротность по таблице: при
resonance = 16384 затухание 10 639 / 32 767 = 0,3247, Q = 3,08;
при 28 000 — 0,0845, Q = 11,83. У аналогового ФНЧ второго порядка с
такой Q пик должен стоять на −46,9 и −3,1 цента ниже fc, высотой 9,89 и
21,47 дБ. Вышло:
| Нота, fc | Q | Пик ФНЧ | На fc | На 4·fc |
|---|---|---|---|---|
| 60, 261,6 Гц | 3,08 | −46,9 цента, 10,00 дБ | 9,89 дБ | −18,86 дБ |
| 84, 1046,5 Гц | 3,08 | −34,4 цента, 9,89 дБ | 9,81 дБ | −21,51 дБ |
| 84, 1046,5 Гц | 11,83 | 0 центов, 21,47 дБ | 21,47 дБ | −20,54 дБ |
| 108, 4186,0 Гц | 11,83 | +6,2 цента, 21,51 дБ | 21,47 дБ | −21,72 дБ |
| 120, 8372,0 Гц | 11,83 | +18,8 цента, 21,71 дБ | 21,47 дБ | −19,17 дБ |
Подъём на fc совпадает с Q по таблице: при Q = 11,83 — от 21,47 до
21,49 дБ против 21,46 дБ на всех нотах 48…120, при Q = 3,08 — от 9,78
до 9,97 дБ против 9,77 дБ. Пик к верху диапазона уходит вверх на десятки центов —
цифровой SVF при fc, сравнимой с частотой отсчётов, уже не аналоговый.
В полосе пропускания (fc / 8) усиление от +0,15 до +0,96 дБ, больше у
низких нот: f мало, и целочисленное состояние округляется грубо.
Выход ни разу не дошёл до насыщения — наибольший отсчёт 12 173.
Свой фильтр и блок. Та же схема Чемберлина в float, расчёт блока — стерео 16 бит, прямо в половину буфера DMA:
void synth_init(synth_t *s, float fs)
{
s->phase = 0.0f;
s->inc = 0.0f;
s->lp = s->bp = 0.0f;
s->f = 0.0f;
s->damp = 1.0f;
s->fs = fs;
}
void synth_set(synth_t *s, float note, float cutoff_note, float q)
{
float fc = note_to_hz(cutoff_note);
if (fc > s->fs / 8.0f) /* выше fs/8 этот фильтр неустойчив — как в Braids */
fc = s->fs / 8.0f;
s->inc = note_to_hz(note) / s->fs;
s->f = 2.0f * sinf(3.14159265f * fc / s->fs);
s->damp = 1.0f / q;
}
/* Фильтр с переменными состояния (Чемберлин): ФНЧ, полосовой и ФВЧ за один проход */
float synth_svf_lp(synth_t *s, float in)
{
float notch = in - s->damp * s->bp;
s->lp += s->f * s->bp;
float hp = notch - s->lp;
s->bp += s->f * hp;
return s->lp;
}
/* Блок для DMA: frames кадров стерео, 16 бит, левый и правый одинаковые */
void synth_render(synth_t *s, int16_t *out, size_t frames)
{
while (frames--) {
float y = synth_svf_lp(s, 0.25f * synth_saw(s));
if (y > 1.0f) y = 1.0f;
if (y < -1.0f) y = -1.0f;
int16_t v = (int16_t)(y * 32767.0f);
*out++ = v;
*out++ = v;
}
}
Объявления и состояние голоса — synth.h:
/* Раздел 18в: маленький голос синтезатора — V/OCT, генератор пилы с polyBLEP,
* фильтр SVF и расчёт блока. Переносимый C: собирается и на компьютере, и под STM32F405. */
#ifndef SYNTH_H
#define SYNTH_H
#include <stdint.h>
#include <stddef.h>
typedef struct { /* калибровка входа V/OCT: нота = x * scale + offset */
float scale, offset;
} cal_t;
typedef struct {
float phase, inc; /* фаза 0…1 и шаг за отсчёт */
float lp, bp; /* состояние фильтра */
float f, damp; /* 2·sin(π·fc/fs) и 1/Q */
float fs;
} synth_t;
float voct_x(int16_t code);
int cal_two_points(cal_t *c, float x_1v, float x_3v);
float voct_to_note(int16_t code, const cal_t *c);
float note_to_hz(float note);
float polyblep(float t, float dt);
void synth_init(synth_t *s, float fs);
void synth_set(synth_t *s, float note, float cutoff_note, float q);
float synth_saw(synth_t *s);
float synth_saw_naive(synth_t *s);
float synth_svf_lp(synth_t *s, float in);
void synth_render(synth_t *s, int16_t *out, size_t frames);
#endif
У float-фильтра подъём на fc — ровно 20 · lg Q: 9,54 дБ при Q = 3 и
21,58 дБ при Q = 12 (fc = 1046,5 Гц, 48 кГц). 2000 блоков по 24 кадра —
пик 16 857, до предела не дошло, каналы одинаковые. Весь synth.c для
Cortex-M4F — 944 байта кода, synth_render — 292.
Цепочка. Пила ноты 48 через ФНЧ с resonance = 20000, частота среза
за секунду едет от ноты 72 до 108, всё блоками по 24 — 4000 блоков,
WAV 96 000 Гц, 16 бит, моно, 96 000 кадров, пик 19 120 без насыщения.
Центр тяжести спектра — 381 Гц в первой четверти и 669 Гц в последней:
фильтр открывается, звук становится ярче.
Срок блока. Всё, что модуль считает, должно уложиться в длительность
блока — иначе DMA догонит ещё не заполненную половину, и будет щелчок.
Частоты и блоки — из кода модулей, такты — из F_CPU:
| Модуль | МК | Частота | Блок | Срок | Тактов на блок | Тактов на отсчёт |
|---|---|---|---|---|---|---|
| Braids | F103, 72 МГц | 96 000 | 24 | 0,250 мс | 18 000 | 750 |
| Plaits | F373, 72 МГц | 48 000 | 12 | 0,250 мс | 18 000 | 1500 |
| Clouds | F405, 168 МГц | 32 000 | 32 | 1,000 мс | 168 000 | 5250 |
| Elements | F405, 168 МГц | 32 000 | 16 | 0,500 мс | 84 000 | 5250 |
| Rings | F405, 168 МГц | 48 000 | 24 | 0,500 мс | 84 000 | 3500 |
| Warps | F405, 168 МГц | 96 000 | 60 | 0,625 мс | 105 000 | 1750 |
Видна логика выбора. Самые тяжёлые алгоритмы (Clouds, Elements) работают
на 32 кГц — так на отсчёт приходится 5250 тактов. Блоки маленькие —
0,25…1 мс: это и задержка отклика на вход управления, а управляющие
входы читаются раз в блок. У Braids на отсчёт всего 750 тактов, и
отсюда целочисленный код и таблицы вместо sin и pow. Сколько тактов
реально тратит каждый модуль, здесь не измерено: платы не было.
18в.7 Грабли
Нота уходит на несколько центов, хотя таблицы точные. Частота
отсчётов не та, что в коде: у F373 делитель I2S целый, и 48 кГц
превращаются в 47 872,34 Гц (−4,61 цента). Считать шаги нот надо от
настоящей частоты, как kCorrectedSampleRate в Plaits.
Строй «плывёт» по октавам. Наклон входа V/OCT задан резисторами:
1 % разницы — около 12 центов на октаву. Лечится только калибровкой по
двум напряжениям (1 В и 3 В) с записью scale и offset во флеш.
Калибровка «съела» ошибку и сохранилась. Без проверки разумности
перепутанные 1 В и 3 В дали бы отрицательный наклон. В Plaits окно
-0.6 < delta < -0.2 не пускает такие данные в память.
Звенит на высоких нотах. Пила или меандр без поправки на скачке: наложение спектра. Полиномиальный BLEP в две строки убирает около 15 дБ; цена — тише верхние гармоники.
Щелчки при тяжёлом режиме. Расчёт блока не уложился в срок.
Прерывание DMA приходит каждые block / fs секунд — у Rings каждые
0,5 мс — и половина буфера должна быть готова к следующему.
Кодек ведущий — звук через отсчёт. В режиме, где такт даёт WM8731,
слоты идут с набивкой; без раздвигания отсчётов (stride) звук
окажется вдвое быстрее или с нулями между отсчётами.
Первый блок после смены формы — не той высоты. В Braids Render
при смене формы зовёт Init(), а та ставит ноту 60. Высоту задают после
первого вызова.
Прошивка не собирается из скачанного архива. Подмодули git
(stmlib, загрузчики) не скачаны: include stmlib/makefile.inc
не находит файл.
18в.8 Итоги
- Модули Mutable Instruments: F405 на 168 МГц с кодеком WM8731 там, где нужен звуковой вход (Clouds, Rings, Elements, Warps); F373 на 72 МГц с ЦАП PCM5100 (Plaits); F103 на 72 МГц с ЦАП DAC8551 по SPI (Braids).
- F405: I2S2 передаёт, I2S2ext принимает, DMA по кругу, прерывание приёма «половина / конец» считает свою половину; у Rings буфер — 96 полуслов. 48 кГц от PLLI2S — 47 991,07 Гц (−0,0186 %).
- F373 без звуковой ФАПЧ: делитель 47, 47 872,34 Гц, −4,61 цента — код учитывает это в расчёте нот.
- V/OCT в Plaits: ОУ с усилением −0,33 и опорой −10 В, вход −2,857…7,143 В, калибровка по 1 В и 3 В даёт scale = −60, offset = 25,714 — ровно значения по умолчанию; 1 % резистора — −11,88 цента на октаву.
- Braids: таблица шага фазы на одну октаву (97 чисел) и сдвиги — ошибка строя не больше 0,0113 цента на всём диапазоне.
- Полиномиальный BLEP: наложение −15,6 → −31,1 дБ у ноты 96, худшая побочная −31,8 → −58,0 дБ.
- SVF Braids: подъём на fc = Q из таблицы (3,08 и 11,83) с точностью до 0,2 дБ.
- Срок блока: от 0,25 мс (18 000 тактов у Braids и Plaits) до 1 мс (168 000 тактов у Clouds).
- Свой голос на C: калибровка по двум точкам — ошибка ноты до 0,193 цента на 0…5 В; polyBLEP в float при 48 кГц — наложение −12,5 → −28,4 дБ; настройка I2S2 + I2S2ext + DMA на F405 — 492 байта кода, голос — 944.
Полные исходники
synth.c — голос синтезатора: приёмы Plaits и Braids, свой код во float.
/* Раздел 18в: голос синтезатора. Приёмы — как в Plaits и Braids, код свой, в float. */
#include <math.h>
#include "synth.h"
/* 16-битный код АЦП -> x от -1 до +1, как float_value() в Plaits */
float voct_x(int16_t code)
{
return (float)code / 32768.0f;
}
/* Калибровка по двум точкам: 1 В -> нота 12, 3 В -> нота 36 (24 полутона на 2 В).
* Возвращает 0 и ничего не меняет, если разность не похожа на правду. */
int cal_two_points(cal_t *c, float x_1v, float x_3v)
{
float delta = x_3v - x_1v;
if (!(delta > -0.6f && delta < -0.2f))
return 0;
c->scale = 24.0f / delta;
c->offset = 12.0f - c->scale * x_1v;
return 1;
}
float voct_to_note(int16_t code, const cal_t *c)
{
return voct_x(code) * c->scale + c->offset;
}
float note_to_hz(float note)
{
return 440.0f * exp2f((note - 69.0f) / 12.0f);
}
/* Поправка к скачку пилы: t — фаза 0…1, dt — шаг фазы за отсчёт */
float polyblep(float t, float dt)
{
if (t < dt) { /* отсчёт сразу после скачка */
t /= dt;
return t + t - t * t - 1.0f;
}
if (t > 1.0f - dt) { /* отсчёт перед скачком */
t = (t - 1.0f) / dt;
return t * t + t + t + 1.0f;
}
return 0.0f;
}
void synth_init(synth_t *s, float fs)
{
s->phase = 0.0f;
s->inc = 0.0f;
s->lp = s->bp = 0.0f;
s->f = 0.0f;
s->damp = 1.0f;
s->fs = fs;
}
void synth_set(synth_t *s, float note, float cutoff_note, float q)
{
float fc = note_to_hz(cutoff_note);
if (fc > s->fs / 8.0f) /* выше fs/8 этот фильтр неустойчив — как в Braids */
fc = s->fs / 8.0f;
s->inc = note_to_hz(note) / s->fs;
s->f = 2.0f * sinf(3.14159265f * fc / s->fs);
s->damp = 1.0f / q;
}
float synth_saw(synth_t *s)
{
float y = 2.0f * s->phase - 1.0f - polyblep(s->phase, s->inc);
s->phase += s->inc;
if (s->phase >= 1.0f)
s->phase -= 1.0f;
return y;
}
float synth_saw_naive(synth_t *s)
{
float y = 2.0f * s->phase - 1.0f;
s->phase += s->inc;
if (s->phase >= 1.0f)
s->phase -= 1.0f;
return y;
}
/* Фильтр с переменными состояния (Чемберлин): ФНЧ, полосовой и ФВЧ за один проход */
float synth_svf_lp(synth_t *s, float in)
{
float notch = in - s->damp * s->bp;
s->lp += s->f * s->bp;
float hp = notch - s->lp;
s->bp += s->f * hp;
return s->lp;
}
/* Блок для DMA: frames кадров стерео, 16 бит, левый и правый одинаковые */
void synth_render(synth_t *s, int16_t *out, size_t frames)
{
while (frames--) {
float y = synth_svf_lp(s, 0.25f * synth_saw(s));
if (y > 1.0f) y = 1.0f;
if (y < -1.0f) y = -1.0f;
int16_t v = (int16_t)(y * 32767.0f);
*out++ = v;
*out++ = v;
}
}
Дальше: раздел 18г «RFID-считыватель RC522: MFRC522 по SPI, поиск карты, UID, антиколлизия, чтение блоков MIFARE Classic» — ещё одно устройство на SPI: считыватель бесконтактных карт.