Генерация звука на микроконтроллерах AVR методом волновых таблиц с поддержкой полифонии

Микроконтроллеры AVR довольно дешевы и широко распространены. Наверно, с них начинает почти любой embedded разработчик. А среди любителей правит балом Arduino, сердцем которого обычно является ATmega328p. Наверняка многие задумывались: как можно заставить их звучать?

Если посмотреть на существующие проекты, то они бывают нескольких типов:

  1. Генераторы квадратных импульсов. Генерация с помощью ШИМ или дергать пины в прерываниях. В любом случае, получается очень характерный пищащий звук.
  2. Использование внешнего оборудования типа MP3 декодера.
  3. Использование ШИМ для вывода 8 битного (иногда 16 битного) звука в формате PCM или ADPCM. Поскольку памяти в микроконтроллерах для этого явно не достаточно, то обычно используют SD карту.
  4. Использование ШИМ для генерации звука на основе волновых таблиц, подобных MIDI.

Последний тип для меня был особенно интересен, т.к. почти не требует дополнительного оборудования. Представляю сообществу свой вариант. Для начала небольшое демо:



Заинтересовавшихся прошу под кат.

Итак, оборудование:

  • ATmega8 или ATmega328. Портировать на другие ATmega не сложно. И даже на ATtiny, но об этом позже;
  • Резистор;
  • Конденсатор;
  • Динамик или наушники;
  • Питание;

Вроде все.

Простая RC цепочка вместе с динамиком подключается к выводу микроконтроллера. На выходе получаем 8 битный звук с частотой дискретизации 31250Гц. При частоте кристалла в 8МГц можно генерировать до 5 каналов звука + один шумовой канал для перкуссии. При этом используется почти все процессорное время, но после заполнения буфера процессор можно занять чем-то полезным помимо звука:


Данный пример полностью помещается в память ATmega8, 5 каналов + шум обрабатываются при частоте кристалла 8МГц и остается немного времени на анимацию на дисплее.

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

А теперь подробности…

Волновые таблицы или wavetables


Математика предельно проста. Есть периодичная функция тона, например tone(t) = sin(t * freq / (2 * Pi)).

Так же есть функция изменения громкости основного тона от времени, например volume(t) = e ^ (- t).

В самом простом случае звучание инструмента – это произведение этих функций instrument(t) = tone(t) * volume(t):

На графике это все выглядит примерно так:



Дальше берем все звучащие в данный момент времени инструменты и суммируем их с некоторыми коэффициентами громкости (псевдокод):

for (i = 0; i < CHANNELS; i++) {
  value += channels[i].tone(t) * channels[i].volume(t) * channels[i].volume;
}

Надо только подбирать громкости так, чтобы не было переполнения. И это почти все.

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

Перкуссия — это микс шумового канала и низкочастотной волны, приблизительно в 50-70 Гц.
Конечно, качественного звука таким образом добиться сложно. Но у нас же всего 8 килобайт на все. Надеюсь, это можно простить.

Что можно выжать из 8 бит


Изначально я ориентировался на ATmega8. Без внешнего кварца она работает на частоте 8МГц и имеет 8 битный ШИМ, что дает базовую частоту дискретизации 8000000 / 256 = 31250Гц. Один таймер использует ШИМ для вывода звука и он же при переполнении вызывает прерывание для передачи следующего значения в генератор ШИМ. Соответственно, у нас 256 тактов для вычисления значения сэмпла на все, включая накладные расходы на прерывание, обновление параметров звуковых каналов, отслеживание времени, когда надо проигрывать очередную ноту и т.д.

Для оптимизации будем активно использовать следующие трюки:

  • Поскольку процессор у нас восьмибитный, то переменные будем стараться делать такими же. Иногда будем пользоваться 16 битными.
  • Вычисления условно разделим на частые и не очень. Первые необходимо вычислять для каждого сэмпла, вторые – заметно реже, раз в несколько десятков/сотен сэмплов.
  • Для равномерного распределения нагрузки во времени мы используем циклический буфер. В основном цикле программы мы буфер наполняем, в прерывании вычитываем. Если все хорошо, то наполняется буфер быстрее, чем опустошается и у нас есть время на что-то еще.
  • Код написан на C с большим количеством inline. Практика показывает, что так заметно быстрее.
  • Все что можно просчитать препроцессором, особенно с участием деления, делается препроцессором.

Для начала, разделим время на промежутки по 4 миллисекунды (я назвал их тиками). При частоте дискретизации 31250Гц получаем 125 сэмплов на тик. То, что обязательно нужно считать каждый сэмпл – будем считать каждый сэмпл, а остальное – раз в тик или реже. Например, в рамках одного тика громкость инструмента будет постоянной: instrument(t) = tone(t) * currentVolume; а сам currentVolume будет пересчитываться раз в тик с учетом volume(t) и выбранной громкости звукового канала.

Длительность тика в 4мс была выбрана исходя из простого 8 битного ограничения: при восьмибитном счетчике сэмплов можно работать с частотой дискретизации до 64кГц, при восьмибитном счетчике тиков мы можем измерять время до 1-й секунды.

Немного кода


Сам канал описывается такой структурой:

typedef struct
{
    // Info about wave
    const int8_t* waveForm; // Wave table array
    uint16_t waveSample; // High byte is an index in waveForm array
    uint16_t waveStep; // Frequency, how waveSample is changed in time

    // Info about volume envelope
    const uint8_t* volumeForm; // Array of volume change in time
    uint8_t volumeFormLength; // Length of volumeForm
    uint8_t volumeTicksPerSample; // How many ticks should pass before index of volumeForm is changed
    uint8_t volumeTicksCounter; // Counter for volumeTicksPerSample

    // Info about volume
    uint8_t currentVolume; // Precalculated volume for current tick
    uint8_t instrumentVolume; // Volume of channel
} waveChannel;

Условно данные тут разделены на 3 части:

  1. Информация о форме волны, фаза, частота.

    waveForm: информация о функции tone(t): ссылка на массив длиной 256 байт. Задает тембр, звучание инструмента.

    waveSample: старший байт указывает на текущий индекс массива waveForm.

    waveStep: задает частоту, на сколько waveSample будет увеличен при подсчете следующего сэмпла.

    Каждый сэмпл считается примерно так:

    int8_t tone = channelData.waveForm[channelData.waveSample >> 8];
    channelData.waveSample += channelaData.waveStep;
    return tone * channelData.currentVolume;

  2. Информация о громкости. Задает функцию изменения громкости от времени. Постольку громкость меняется не так часто, то пересчитывать ее можно реже, раз в тик. Делается это примерно так:

    if ((channel->volumeTicksCounter--) == 0 && channel->volumeFormLength > 0) {
        channel->volumeTicksCounter = channel->volumeTicksPerSample;
        channel->volumeFormLength--;
        channel->volumeForm++;
    }
    channel->currentVolume = channel->volumeForm * channel->instrumentVolume >> 8;

  3. Задает громкость канала и посчитанную текущую громкость.

Обратите внимание: форма волны – восьмибитная, громкость – тоже восьмибитная, а результат – 16 битный. С небольшой потерей производительности можно сделать звук (почти) 16 битным.

При борьбе за производительность пришлось прибегнуть к некоторой черной магии.

Пример номер 1. Как пересчитывать громкость каналов:

if ((tickSampleCounter--) == 0) {
    // Наступил новый тик
    tickSampleCounter = SAMPLES_PER_TICK – 1;
    // Посчитать еще что-то
}
// volume recalculation should no be done so often for all channels
if (tickSampleCounter < CHANNELS_SIZE) {
    recalculateVolume(channels[tickSampleCounter]);
}

Таким образом, все каналы пересчитывают громкость раз в тик, но не одновременно.

Пример номер 2. Держать информацию о канале в статической структуре дешевле, чем в массиве. Не вдаваясь в подробности реализации wavechannel.h скажу, что этот файл вставляется в код несколько раз (равное количеству каналов) с разными директивами препроцессора. При каждой вставке создаются новые глобальные переменные и новая функция расчета канала, которая потом инлайнится в основной код:

#if CHANNELS_SIZE >= 1
    val += channel0NextSample();
#endif
#if CHANNELS_SIZE >= 2
    val += channel1NextSample();
#endif
…

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

while ((samplesToWrite) > 4) { // Ждем в цикле пока буфер не будет почти заполнен
    fillBuffer(SAMPLES_PER_TICK); // Наполняем буфер в течение какого-то времени
    updateMusicData(); // Обновляем состояние нот
}

По-хорошему надо бы еще донаполнить буфер после цикла, но, поскольку у нас почти все inline, то заметно раздувается размер кода.

Музыка


Используется восьмибитный счетчик тиков. При достижении нуля начинается новый такт, счетчику присваивается длительность такта (в тиках), чуть позже проверяется массив музыкальных команд.

Музыкальные данные хранятся в массиве байтов. Записывается примерно так:

const uint8_t demoSample[] PROGMEM = {
    DATA_TEMPO(160), // Set beats per minute
    DATA_INSTRUMENT(0, 1), // Assign instrument 1 (see setSample) to channel 0
    DATA_INSTRUMENT(1, 1), // Assign instrument 1 (see setSample) to channel 1
    DATA_VOLUME(0, 128), // Set volume 128 to channel 0
    DATA_VOLUME(1, 128), // Set volume 128 to channel 1
    DATA_PLAY(0, NOTE_A4, 1), // Play note A4 on channel 0 and wait 1 beat 
    DATA_PLAY(1, NOTE_A3, 1), // Play note A3 on channel 1 and wait 1 beat
    DATA_WAIT(63), // Wait 63 beats
    DATA_END() // End of data stream
};

Все что начинается с DATA_ – препроцессорные макросы, которые разворачивают параметры в необходимое количество байт данных.

Например, команда DATA_PLAY разворачивается в 2 байта, в которых хранятся: маркер команды (1 бит), пауза перед следующей командой (3 бита), номер канала, на котором играть ноту (4 бита), информация о ноте (8 бит). Самое существенное ограничение – этой командой нельзя делать большие паузы, максимум 7 тактов. Если надо больше, то надо использовать команду DATA_WAIT (до 63 тактов). К сожалению, я так и не нашел, можно ли макрос развернуть в разное количество байт массива в зависимости от параметра макроса. И даже warning не знаю как вывести. Может быть вы подскажите.

Использование


В каталоге demos есть несколько примеров под разные микроконтроллеры. Но если коротко, то вот кусок из readme, мне добавить особо нечего:

#include "../../microsound/devices/atmega8timer1.h"
#include "../../microsound/micromusic.h"

// Make some settings
#define CHANNELS_SIZE   5
#define SAMPLES_SIZE    16
#define USE_NOISE_CHANNEL

initMusic(); // Init music data and sound control
sei(); // Enable interrupts, silence sound should be generated
setSample(0, instrument1); // Use instrument1 as sample 0
setSample(1, instrument2); // Init all other instruments…

playMusic(mySong); // Start playing music at pointer mySong
while (!isMusicStopped) {
    fillMusicBuffer(); // Fill music buffer in loop
    // Do some other stuff
}

Если хочется еще что-то делать помимо музыки, то можно увеличить размер буфера с помощью BUFFER_SIZE. Размер буфера должен быть 2^n, но, к сожалению, при размере в 256 происходит падение производительности. Пока не разобрался с этим.

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

Всякие delay не рекомендуется использовать, т.к. процессорное время расходуется впустую. Вместо этого реализован свой метод в файле microsound/delay.h, который помимо самой паузы занимается наполнением буфера. Данный метод может работать не очень точно на коротких паузах, но на длинных более-менее вменяемо.

Создание своей музыки


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

Существует довольно забавный сервис wavepot.com – онлайн редактор JavaScript, в котором надо задать функцию звукового сигнала от времени, и этот сигнал выводится на звуковую карту. Простейший пример:

function dsp(t) {
  return 0.1 * Math.sin(2 * Math.PI * t * 440);
}

Я портировал движок на JavaScript, он находится в demos/wavepot.js. Содержимое файла надо вставить в редакторе wavepot.com и можно проводить эксперименты. Пишем свои данные в массив soundData, слушаем, не забываем сохранять.

Отдельно стоит упомянуть о переменной simulate8bits. Она, согласно названию, симулирует восьмибитный звук. Если вдруг покажется, что барабаны гудят, а в затухающих инструментах при тихом звуке появляются помехи, то это оно, искажение восьмибитного звука. Можно попробовать отключить эту опцию и послушать разницу. Проблема гораздо менее ощутима, если в музыке нет тишины.

Подключение


В простом варианте схема выглядит так:

+5V    
 ^      MCU
 |   +-------+
 +---+VC     |    R1
     |    Pin+---/\/\--+-----> OUT
     |       |         |
 +---+GN     |        === C1
 |   +-------+         |
 |                     |
--- Grnd              --- Grnd

Выходной пин зависит от микроконтроллера. Резистор R1 и конденсатор C1 надо подбирать исходя из нагрузки, усилителя (если есть) и т.д. Я не электронщик и приводить формулы не буду, их легко нагуглить вместе с онлайн калькуляторами.

У меня R1 = 130 Ом, С1 = 0.33 мкФ. На выход подключаю обычные китайские наушники.

Что там было про 16 битный звук?


Как я говорил выше, при умножении двух восьмибитных чисел (частота и громкость) мы получаем 16 битное число. Его можно не округлять до восьмибитного, а выводить оба байта в 2 ШИМ канала. Если эти 2 канала смешать в пропорции 1/256, то мы можем получить 16 битный звук. Разницу с восьмибитным особенно легко услышать на плавно затухающих звуках и барабанах в моменты, когда звучит только один инструмент.

Подключение 16 битного выхода:

+5V    
 ^      MCU
 |   +-------+
 +---+VCC    |    R1
     |   PinH+---/\/\--+-----> OUT
     |       |         |
     |       |    R2   |
     |   PinL+---/\/\--+
 +---+GND    |         |
 |   +-------+        === C1
 |                     |
--- Grnd              --- Grnd

Здесь важно правильно смешать 2 выхода: сопротивление R2 должно быть в 256 раз больше сопротивления R1. Чем точнее, тем лучше. К сожалению, даже резисторы с погрешностью 1% не дают требуемой точности. Однако, даже с не очень точным подбором резисторов искажения можно заметно ослабить.

К сожалению, при использовании 16 битного звука проседает производительность и 5 каналов + шум уже не успевают обрабатываться за отведенные 256 тактов.

А на Arduino можно?


Да, можно. У меня только китайский клон nano на ATmega328p, на нем работает. Скорее всего другие ардуины на ATmega328p тоже должны работать. ATmega168 вроде бы имеет те же регистры управления таймерами. Скорее всего на них будет работать без изменений. На других микроконтроллерах надо проверять, возможно потребуется дописать драйвер.

В demos/arduino328p есть скетч, но чтобы он нормально открылся в Arduino IDE, его нужно скопировать в корень проекта.

В примере генерируется 16 битный звук и используются выходы D9 и D10. Для упрощения можно ограничиться 8 битным звуком и использовать только один выход D9.

Поскольку почти все ардуины работают на 16МГц, то, при желании, можно увеличить количество каналов до 8.

А что с ATtiny?


В ATtiny нет аппаратного умножения. Программное умножение, которое использует компилятор дико медленное и его лучше не использовать. При использовании оптимизированных ассемблерных вставок производительность падает раза в 2 по сравнению с ATmega. Казалось бы, смысла использовать ATtiny нет вообще, но…

На некоторых ATtiny есть умножитель частоты, PLL. А это значит, что на таких микроконтроллерах есть 2 интересные особенности:

  1. Частота генератора ШИМ 64МГц, что дает период ШИМ в 250кГц, что гораздо лучше, чем 31250Гц при 8 МГц или 62500Гц с кварцем на 16 МГц на любых ATmega.
  2. Этот же умножитель частоты позволяет без кварца тактовать кристалл на 16 МГц.

Отсюда вывод: некоторые ATtiny использовать для генерации звука можно. Они успевают обрабатывать те же 5 инструментов + шумовой канал, но на 16 МГц и им не нужен внешний кварц.

Минус в том, что больше частоту уже не повысить, а вычисления занимают почти все время. Для освобождения ресурсов можно уменьшать количество каналов или частоту дискретизации.

Еще один минус в необходимости использования сразу двух таймеров: один для ШИМ, второй для прерывания. На этом, обычно, таймеры и заканчиваются.

Из известных мне микроконтроллеров с PLL могу упомянуть ATtiny85/45/25 (8 ног), ATtiny861/461/261 (20 ног), ATtiny26 (20 ног).

Что касается памяти, то разница с ATmega не велика. В 8кб вполне поместится несколько инструментов и мелодий. В 4кб можно поместить 1-2 инструмента и 1-2 мелодии. В 2 килобайта что-то поместить сложно, но если очень хочется, то можно. Надо разинлайнивать методы, отключать некоторые функции типа контроля громкости по каналам, уменьшать частоту дискретизации и количество каналов. В общем, на любителя, но рабочий пример на ATtiny26 есть.

Проблемы


Проблемы есть. И самая большая проблема – это скорость вычислений. Код полностью написан на C с небольшими ассемблерными вставками умножения для ATtiny. Оптимизация отдается компилятору и он иногда ведет себя странно. При небольших изменениях, которые вроде бы не должны ни на что влиять, можно получить заметное просаживание производительности. Причем изменение с -Os на -O3 не всегда помогает. Один из таких примеров – использование буфера размером 256 байт. Особенно неприятно то, что нет гарантии, что в новых версиях компилятора мы не получим падение производительности на том же коде.

Другая проблема в том, что совсем не реализован механизм затухания перед следующей нотой. Т.е. когда на каком-то канале одна нота сменяется другой, то старое звучание резко прерывается, иногда слышен небольшой щелчок. Хотелось бы найти способ избавиться от этого без потери производительности, но пока так.

Нет команд для плавного нарастания/затухания громкости. Особенно критично для коротких мелодий-уведомлений, где в конце надо сделать быстрое затухание громкости, чтобы не было резкого обрыва звучания. Частично проблема обходится написанием череды команд с ручной установкой громкости и короткой паузы.

Выбранный подход в принципе не способен обеспечить натуралистичное звучание инструментов. Для более натуралистичного звучания нужно разделить звуки инструментов на attack-sustain-release, использовать хотя бы первые 2 части и с гораздо большей длительностью, чем один период колебания. Но тогда данных для инструмента потребуется гораздо больше. Была идея использовать более короткие волновые таблицы, например в 32 байта вместо 256, но без интерполяции сильно падает качество звука, а с интерполяцией падает производительность. А еще 8 бит дискретизации явно мало для музыки, но это можно обойти.

Размер буфера ограничен в 256 сэмплов. Это соответствует примерно 8 миллисекундам и это максимальный цельный промежуток времени, который можно отдать другим задачам. При этом выполнение задач все равно периодически приостанавливается прерываниями.

Замена стандартного delay работает не очень точно на коротких паузах.

Уверен, что это не полный список.

Ссылки


Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 69

    0
    Класс! Отличная статья, всё очень хорошо описано!
      –3

      Все это конечно замечательно. НО зачем?


      Цена, размер корпуса да и потребление в этом режиме 32-х битных контроллеров практически те же что и на ATMega.


      Почему все так упорно пользуются древними ATMega?

        +2
        ATMega, да и вообще все AVR, обладают огромным преимуществом перед 32-битными МК в плане обучения и легкости вхождения в работу с ними. Сколько всего надо прописать в исходник чтоб заставить хотя бы помигать светодиодом на каком-нибудь STM32? И сравнить это с аналогичным решением на AVR.
        Ну а те кому перестанет хватать мощности ATMega, с полученным багажом знаний уже можно переходить на STM32 и иже с ними.
          0
          Сколько всего надо прописать в исходник чтоб заставить хотя бы помигать светодиодом на каком-нибудь STM32?

          Немного… (инициализация шаблонно). Если через напрямую в стиле "записать в порт" то практически столько же как на AT DDRD |= _BV(PD0); PORTD &= ~_BV(PD0);
          (что не очень хороший стиль. ибо код не переносим может оказаться)


          За то проблемы "остается немного времени на анимацию на дисплее" не беспокоят.


          #define DBG_LED_PIN GPIO_Pin_13
          #define DBG_LED_PORT GPIOC
          .....
                  GPIO_InitTypeDef GPIO_InitStructure;
              GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
              GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
              GPIO_InitStructure.GPIO_Pin = DBG_LED_PIN; GPIO_Init(DBG_LED_PORT, &GPIO_InitStructure);
          .....
          //GPIO_SetBits(DBG_LED_PORT, DBG_LED_PIN); GPIO_ResetBits(DBG_LED_PORT, DBG_LED_PIN);
                  if ( flip ) {
                      DBG_LED_PORT->BSRR = DBG_LED_PIN; 
                  } else {
                      DBG_LED_PORT->BRR = DBG_LED_PIN;
                  }
          

          И да. "Наверно, с них начинает почти любой embedded разработчик." не совсем правильное утверждение. Если, конечно, под "embedded разработчик" не понимать "я тут светодиодом помигал".


          Документация от STM ничем не сложнее чем от Atmel.

            0
            Ну, как бы не совсем так. Сначала надо раскочегарить ядро, выставить всяческие делители клоков, включить PLL, подождать пока он стабилизируется, подать клоки на периферию, а уж потооооом… можно ногами шевелить
              0

              Это шаблонный код, кочующий из проект в проект. И не так что бы сложный для понимания.
              Если не интересует потребление, то можно вообще все модули включить.


              Чем мне не нравится Arduino как концепция… Кто то дернул чью то библиотеку (зачастую корявую и на оптимизированную) и все. Эмбедет разработчик в резюме.

                0
                Зато проверять датчики просто, купил arduino nano, купил нужный датчик, скачал либу и за 10 минут он уже работает.
                Не устроил датчик, много времени не потерял.
                Устроил датчик, применил в своей схеме, либу портировал с ардиуно или свою написал.
            0
            Тут больше всего преимущество в ардуино и менеджере библиотек.
            Подключить почти любой датчик или экран к ардуино не проблема в течении 15 минут, тогда как на STM32 с SPL или HAL надо искать пример в гугл и ещё адаптировать его под себя…
              0
              Справедливости ради, у ti есть несколько ланчпэдов, в том числе и с 32битными чипами, поддерживаемые Ардуино-подобной ИДЕшкой Energia.
                0
                а менеджер библиотек в ней есть? библиотеки от ардуино подходят?
                  0
                  Менеджер есть, от ардуины не подходят.
                0

                Ардуино умеет не только в avr, а, скажем, stm32 и esp32 — тоже

                  0
                  Может, но для STM32 надо самому шить загрузчик или пользоваться для заливки STlink, что не очень удобно. И проблемы с несовместимостью библиотек.

                  Для esp8266 вполне норм ардуино.

                  Для esp32 по моему лучше всё такие юзать нормальную freertos. Но дело вкуса конечно)

                +1
                1) Скачать и установить CubeMX
                2) Скачать на выбор одну из IDE, которую этот CubeMX умеет.
                3) В CubeMX установить (в один клик почти) поддержку желаемого семейства камней.
                4) В красивом и безобразно простом GUI интерфейсе выставить какие пины используем.
                5) Настроить тактирование (или забить на это, всёравно оно будет настроено), кем и откуда, ну и ввести частоту — куб всё сам сделает
                6) Нажать кнопочку Generate
                7) Откроется IDE, в которой в цикл while(1) написать что-то типа
                HAL_GPIO_TogglePin(MYLED_Port, MYLED_Pin);
                HAL_Delay(500);
                

                8) Настроить программатор (тут от IDE зависит) — тоже пара кликов.
                9) Нажать кнопочку «Собери мне это и запусти»
                10) Насладиться мигающим светодиодом.
                  0
                  А теперь меняем светодиод на WS2812B…
                    0
                    Гугл мне в ответ приводит 100500 уроков/библиотек/тем на форумах и т.п. Не вижу принципиально никаких отличий от AVR. Если мы приплетаем ардуину(чур меня!), то оная зараза и на STM32 есть.
                      0
                      то оная зараза и на STM32 есть.

                      Есть, только либы не все подходят

                      Не вижу принципиально никаких отличий от AVR.

                      Принципиальная разница в наличии менеджера библиотек
                        0
                        Вы про Atmel-овский ASF?
                          0
                          Если честно я атмелы знаю плохо, а стм32 довольно хорошо (были проекты от f0, f1… до f7, h7)

                          я не знаю, что такое Atmel ASF

                          Но если есть платка ардуино нано(например), то я допустим могу потестить в течении очень короткого времени какие то SPI/I2C дисплеи, датчики температуры/влажности/давления/расстояние/освещения.

                          То есть код не надо писать вообще, скачивается ардуино иде, скачивается библиотека, выбирается платка ардуино нано и подключается девайс к ардуинке. Всё

                          Ну разве это не прикольно?

                          А в стм32 пока с шиной i2c разберёшься, пройдёт год
                            0
                            ASF — это атмеловские либы в Atmel Studio, включаются в проект примерно также, как и в STM32 HAL-е, только в STM это идёт через CubeMX, а у атмела уже в IDE.

                            С STM32 и их i2c я крупный гемор поимел только один раз — на F103-ей какая-то версия куба ставила включение тактирования в конец инициализации, а не в начало, благодаря чему вылез глюк вида «дотронулся щупом/пальцем при включении — работает, не дотронулся — линия притянута к нулю и не работает».
                            Вариант «быстренько накидать готовых либ» я не люблю — там бывает очень много не просто индусского кода, а индусского говнокода. Да и когда я мучал множество i2c барахла, то как правило портирование ардуиновской библиотеки особо много времени не занимало.
                              0
                              только в STM это идёт через CubeMX, а у атмела уже в IDE.

                              Ну не, я про менеджер библиотек. CubeMX это не то
                              В иде можно поиск сделать, по названию датчика. Сразу установить и пользоваться
                              Скрытый текст




                              Вариант «быстренько накидать готовых либ» я не люблю — там бывает очень много не просто индусского кода, а индусского говнокода.

                              Да по фиг, я же его не в продакшн, а потестить

                              то как правило портирование ардуиновской библиотеки особо много времени не занимало.


                              Ну одно дело сидеть и портитовать, другое дело, включить и сразу посмотреть как работает. Может и портировать то не надо, а датчик хлам.
                                0
                                Ну не, я про менеджер библиотек. CubeMX это не то
                                В иде можно поиск сделать, по названию датчика. Сразу установить и пользоваться

                                А, понятно, прямо как ASF.
                                Может и портировать то не надо, а датчик хлам.

                                Впринципе тоже вариант, хотя можно в даташыт глянуть, чтобы вообще не покупать.
                                  0
                                  хотя можно в даташыт глянуть

                                  Можно, но всегда хочется пощупать. Или выбрать лучший.
                                  К тому же вдруг на stm32 не заводится, по непонятным причинам. Подключаем к ардуине и проверяем.

              0
              Хорошая статья. Единственное замечание, что WaveTable — это синтез по сэмплам. То есть, основной тон представлен не периодической функцией, а таблицей значений амплитуды.
                0

                Из вики:


                Wavetable synthesis is fundamentally based on periodic reproduction of an arbitrary, single-cycle waveform.

                По сути, тут периодическая функция — это не совсем функция, а как раз таблица значений амплитуды, 256 значений на период. А еще она работает не сама по себе, а в паре с функцией громкости от времени, которая тоже яавляется таблицей значений.
                Возможно, существует более подходящий термин, но я его не знаю.

                  0
                  Я не про код, а про описание в статье с графиком. Сэмпл не обязательно должен быть периодическим: фаза sustain требуется не всегда.
                    0

                    Спасибо за комментарий. Возможно, стоит как-то переименовать заголовок, чтобы не смущать тех, кто в теме. Я старался упростить для тех, кто не в теме, и описал только то, что используется.

                      0
                      > фаза sustain требуется не всегда
                      она может формально присутствовать, но иметь нулевую продолжлительность
                  0
                  www.1bitsymphony.com

                  О чо вспомнил!
                  image

                  Весьма кстати интересный альбом с точки зрения минимализма.
                  Не чиптюн и прочие марио.
                  Жаль, что автор перестал продавать ассемблированый листинг с албомом и бинарник прошивки я так и не нашел
                  А то я хотел сделать его альбом в виде кирпича из эпоксидки с солнечной батарей, шоб жило веками. =)
                    +2
                    Звучит оно ужасно, ИМХО )
                      0
                      Автор расстраивает других минималистов, которые понаписали и понаделали десятки альбомов (чиптюн и прочие марио) и сотни листингов, а никто и не знает. Поверхностно:

                      z80.i-demo.pl
                      shiru.untergrund.net/1bit
                      randomflux.info/1bit
                      0
                      Пару раз делал подобное, каждый раз расстраивало то, что ЦАП на ШИМ заметно свистит, причём не ожидаемая несущая, которую можно задавить ФНЧ, а разные высокие частоты в слышимом диапазоне в зависимости от проигрываемого звука (одиночных нот).

                      Для подавления щелчков делал такое простое решение: в момент выключения сэмпла запоминаем последнее выходное значение в переменную, которую добавляем к выводу, как ещё один канал, и плавно приводим её к 0 за короткое время. Если выход стерео, надо по переменной для каждого канала, а для моно хватит и одной на все каналы. То есть не фейдим продолжающий звучать сэмпл по честному (нужны умножения и больше каналов), а как бы компенсируем смещение нулевой амплитуды (нужно условие и декремент).
                        0

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


                        По поводу PWM — да, проблема есть. Но в коде именно за вывод звука отвечает драйвер (каталог microsound/devices), можно что-то другое подключить. Covox, например, как советуют ниже.

                          0
                          Уменьшение громкости без увеличения полифонии я пробовал — на этапе препроцессинга уменьшал длительности слитных нот на пару кадров, и все ноты играли с коротким фейдом в конце. В принципе это помогает, но либо щелчки всё же проскакивают (недостаточное уменьшение длительности, затухание не всегда заканчивается), либо это начинает довольно заметно влиять на звучание, теряется атака и читаемость партий со слитными нотами, например непрерывного басового галопа или элементарного октавного шагающего баса. Но это лучше, чем щелчки, просто нужно учитывать при выборе/написании музыки.
                            0

                            Следите за фазой на стыке нот

                          0
                          Если посмотреть на существующие проекты, то они бывают нескольких типов:

                          вы забыли (сомневаюсь, что не знали) COVOX
                          image
                            0

                            Картинка не вставилась.
                            Но идея хороша, можете попробовать :)
                            Надо написать драйвер (каталог microsound/devices) и там просто выводить значение в порт.

                            0
                            Отличная статья, спасибо!

                            Однако к ATTINY-25/4585 можно подключить SD и через PWM вполне нормально играть WAV-ки с частотой до 32000 гц :)
                              0

                              О такой возможности я писал во вступлении.

                              +2
                              При таблично-волновом синтезе есть проблема при использовании единственной таблицы так как при ускоренном проигрывании семплов из таблицы верхние гармоники могут подниматься в зону частот выше частоты Найквиста, поэтому возникает алиазинг. Используете ли вы одну волновую таблицу для всех частот или как-то решаете эту проблему? Используется ли интерполяция между значений соседних семплов?
                                0

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


                                Интерполяцию значений между семплами можно включить (директива INTERPOLATE_WAVE), но производительность падает. Решил просто использовать более длинную таблицу в 256 значений, а интерполяцию провести перед помещением данных в таблицу.

                                  0
                                  Понятно, спасибо ) Интересно было бы измерить уровень искажений в зависимости от того используется ли одна таблица или отдельная таблица для каждой ноты, применяется или не применяется интерполяция. Понятно что из-за ограничений на ресурсы AVR сложно воспроизвести сигнал совсем без искажений.
                                    0

                                    Проект на гитхабе, можете попробовать померять.
                                    Укажите 3 канала вместо 5 и можно включить интерполяцию. Или использовать Arduino на 16МГц как самый простой вариант.


                                    Как мне кажется, смысла хранить по таблице для каждой ноты нет. Можно попробовать хранить по таблице на октаву. В каждой следующей уменьшать количество гармоник вдвое путем выбрасывания половины сэмплов а потом интерполировать обратно до 256 сэмплов. Это как самый простой вариант.

                                      0
                                      Давайте рассмотрим такой случай. Допустим нужно воспроизвести сигнал с частотой f такой что количество гармоник ниже частоты Найквеста равно 100. Если хранить bandlimited таблицы на каждую октаву то получиться что в двух соседних таблицах будет 64 и 128 гармоник. Если выбрать первую таблицу то на выходе будет не хватать 36 верхних гармоник, если вторую, то верхние 28 будут вносить вклад в aliasing. Поэтому в идеальном случае нужна таблица на каждое количество гармоник попадающих в выходной сигнал. Если минимальная частота ноты 100 Гц, то при частоте дискретизации 44100 Гц, количество таблиц для квадратной волны будет 22050 / 100 = 220, что довольно много. И при уменьшении минимальной частоты количество будет расти как 1 / freq.
                                  0
                                  А-а-а-а! Так вот, для чего при описании инструментов на разные октавы ипользуется несколько одинаково звучащих семплов. В мануале к AWE32 этот вопрос никак не поднимался. Спасибо за коментарий.
                                  0
                                  Вспомнилось, что подобный программный табличный синтезатор был сделан в начале 2000-х в поздних телефонах с определителем номера, на 51-ом контроллере на 8 МГц. Шесть голосов с довольно приличным звучанием.
                                    0

                                    Не могу открыть ссылку на работе, но почему-то более чем уверен, что там Русь-28

                                    0

                                    Спектрумом повеяло:)

                                      0
                                      А чем мелодии конвертировали/создавали, неужели ручками? Есть какие-то конверторы готовые из MIDI?
                                        0

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


                                        Писать конвертор из midi нет желания и вряд ли получится. Слишком специфические инструменты, ограничение на количество каналов и дискретизацию по тактам. Я думаю, что проще из трекерной музыки (например mod) конвертировать, но и там будет специфика.

                                          0
                                          Можно и mod — это неважно, главное минимум ручной работы. :) А то сидеть перебивать ручками с нотного стана — не комильфо.
                                          За статью спасибо.
                                          Пытался делать нечто подобное, но без ШИМ-а аппаратного, чтобы не было привязки к ногам МК, упирается всё конечно в производительность.
                                          На счет занимать МК ещё другими задачами — необязательно, особенно, если делать на Тиньках, можно считать данное решение как «музыкальный сопроцессор», синтезатор. :)
                                        +1
                                        Не хватает ссылок на Andy FarnellDesigning Sound и на программный синтез звука 1, 2
                                          0
                                          Знающие люди, подскажите…
                                          Давно морочюсь идеей, сделать проигрыватель для винила из оптической мышки. В режиме сканера декодить изображение и получать из него звук. Куда копать?
                                            0
                                            Идея конечно интересная, но сенсор оптической мышки содержит не только камеру, но и DSP, который наружу выдаёт только дельту перемещения, а не сам снимок поверхности. Т.е. можно определить, в какую сторону вращается пластинка, но и только.
                                              +1
                                              На самом деле, можно: habr.com/ru/post/128972
                                              Но хватит ли разрешения?
                                                0
                                                Интересно, не знал. Погуглил, всё равно очень много сомнений, что что-то получится. Чтение растра там реализовано в отладочных целях, читается один пиксель за кадр (324 кадра для чтения всей матрицы), и для канавки нужно разрешение порядка 0.015 мм (стандарт точки касания для иглы LP), а у сенсора размер пикселя непонятный (не пишут), но в штатной конструкции без дополнительных линз явно сильно больше.
                                              0
                                              Затея гиблая, можете почитать, как «хорошо» работали лазерные проигрыватели винила в 80ые и как для них приходилось вычищать пластинки.
                                                0
                                                +2
                                                В свете универсальности/легкости использования, есть уже реализованный проект играющий стандартные 4-х канальные MOD-ы на ATMega16@16MHz.
                                                характеристики MOD Player for AVR 8-bit Atmega
                                                Minimum requirements for hardware:
                                                — MCU: ATmega AVR
                                                — Fclk: 16 MHz
                                                — Flash: 8kB
                                                — SRAM: 1kB
                                                — 2 x 8-bit counter/PWM
                                                — 1 x 16-bit counter

                                                Specifications for player routine:
                                                — 4 channel 31 samples modules support
                                                — volume setting 0-64 for each channel
                                                — 2 stereo channels 8-bit PWM output
                                                — software linear interpolation
                                                — selectable output samplerate up to 48kHz
                                                — most used effect commands supported

                                                The basic idea was to create an all-in-one implementation
                                                without use of external components. On-board PWM's are used
                                                as DAC's, the module is kept in Program Flash.
                                                Tha player is written in pure Assembler.
                                                The current release includes conditional compilation
                                                for ATmega16 and ATmega128 (those author has in presence),
                                                but is also designed for very easy compilation for any other ATmega type.
                                                Also, there is a technological clearance for use of external memory,
                                                RS232 interface for communicational purposes etc.

                                                The schematics for this project is very simple. You just power the chip, supply the 16MHz clock to it and have LEFT and RIGHT audio from 8-PWM's. All specific pin numbers depend on chip, that you selected.
                                                  0

                                                  Интересный проект, хотя мне не очень понятно как его можно использовать. Слишком уж сложно заставить микроконтроллер делать что-то еще помимо воспроизведения музыки.
                                                  К тому же, формат MOD слишком тяжелый для маленьких микроконтроллеров, хоть и намного более универсальный.
                                                  А вот если бы его на Arduino портировать, то могло бы быть вполне годно. Там и 32кб флеша и 16МГц кварц из коробки.

                                                    0
                                                    Может тогда осетра урезать? Посмотреть в сторону Эмулятор AY-8910 на ATMega и на его развитие Эмулятор AY-3-8910/AY-3-8912/YM2149F?
                                                      0

                                                      Интересно, но тут задачи совсем другие.
                                                      В том проекте задача эмулировать чип с определенными характеристиками и для этого выделяются отдельные чипы.
                                                      Я же хотел иметь возможность добавить звук в существующие проекты без существенного изменения схемы. А качество звука и количество каналов настраивать по остаточному принципу, в зависимости от того, сколько ресурсов останется.

                                                        0
                                                        И все таки посмотрите исходники эмулятора, вдруг чтото ценное для себя почерпнете.
                                                  0

                                                  А такой вопрос в чем проблема ATTiny? Ведь мы всё равно умножаем на число меньше 1 и затухание можно будет релиозовать путем вычитания сдвинутого регистром семпла.

                                                    0

                                                    Я не уверен, что правильно понял вопрос. Скорее всего, вы имеете в виду что-то типа:


                                                    outSample = instrumentSample — (instrumentSample >> n);

                                                    Так не получится, так как:


                                                    1. При сдвиге вправо значение уменьшается в 2 раза, а мы можем хотеть иметь громкость, например, 87%.
                                                    2. Если упростить, то один сдвиг соответствует умножению на число, у которого только один бит равен 1, а остальные 0. Если нужно иметь возможность вычислять сэмпл для любого значения громкости, то надо будет делать до 8 сдвигов и мы получим то же умножение, только медленное.
                                                    3. И вообще, в AVR нет операции сдвига на произвольное количество битов. Можно только на 1 бит влево или на 1 бит вправо.
                                                    4. Запоминать уменьшеное значение семпла для уменьшения его позже тоже не получится. Wave table находится в програмной памяти и доступна только для чтения. И ее нужно было бы пересчитывать всю. И хранить отдельно для каждого канала.
                                                      0
                                                      Да вы меня правильно поняли.

                                                      1.Громкость в 87% примерно равна 1 — 1/8 то есть 3 сдвига, да я правда тут не подумал над тем если у нас число будет меньше чем 1/8.

                                                      2.На счет 8 сдвигов как по мне слишком много уже при 4 сдвигах достигается точность в 6,25%, а так как число 8 битное больше делать думаю не будет иметь смысла ну 5 сдвигов ну 3,125%.

                                                      3.На счет этого не знал.
                                                      А так очень классная статья!!!

                                                    0
                                                    Есть прекрасный проект www.avray.ru — эмуляция двух ay-3-8910 на avr. Рекомендую взглянуть.
                                                      0

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

                                                    Only users with full accounts can post comments. Log in, please.