Как реализовать звук на attiny88
Перейти к содержимому

Как реализовать звук на attiny88

  • автор:

Генерация звука на микроконтроллерах 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):

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

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

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

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

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

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

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

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

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

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

Немного кода

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

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

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

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

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

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

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

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

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

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

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

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

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

Музыка

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

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

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

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

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

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

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

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

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

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

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

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

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

UPD: На данный момент wavepot сильно поменялся и мой код с ним не совместим. Можно локально поднять старую версию с github, но она работает только с firefox, а в chrome не выводит звук.

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

Подключение

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

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

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

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

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

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

Здесь важно правильно смешать 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 МГц.

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

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

Из известных мне микроконтроллеров с 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 миллисекундам и это максимальный цельный промежуток времени, который можно отдать другим задачам. При этом выполнение задач все равно периодически приостанавливается прерываниями.

Technoblogy

For a recent project based on the ATtiny85 I wanted to play some simple tones through a piezoelectric speaker, but unfortunately the Arduino tone() function isn’t supported on the ATtiny85 because it doesn’t have the appropriate timers. I therefore needed to find a replacement way of generating simple tones.

There are several existing tone libraries for the ATtiny85, but they all seemed overcomplicated for what I wanted. This post describes my simple TinyTone() function which takes advantage of the ATtiny85’s prescaler to provide a compact tone routine that only needs a table of 12 divisors.

To test this application I used SparkFun’s Tiny AVR Programmer (available in the UK from Proto-PIC [1] ), which lets you program the ATtiny45 or ATtiny85 using the Arduino development environment [2] .

TinyTone.jpg

TinyTone procedure

Here’s how my TinyTone() function works.

The ATtiny85 contains two timers, referred to as Timer/Counter0 and Timer/Counter1. The first of these is already used by the delay() function, so I’ve used Timer/Counter1. This divides the clock frequency, which is either 1MHz or 8MHz, by a prescaler, and then by a one-byte counter. The square wave is output on digital output 1.

Because the prescaler divides the clock by a power of two it provides a convenient way of generating the octave. So TinyTone() is written to take three parameters; the note divisor, the octave number, and the duration in milliseconds [3] :

We then just need a table of divisors for the notes within one octave. To calculate the divisor for a given note frequency we first work out:

divisor = clock / frequency

For example, C4 (middle C) is 261.63Hz, so we get:

divisor = 1000000 / 261.63

However, the divider for the counter must fit into 8 bits, so the maximum number we can represent is 255. The solution is to set the prescaler to divide the clock by 2^4, or 16. Then we get:

divisor = 1000000 / 16 / 261.63

= 238.89 which rounds to 239.

We will define a constant for each divisor to save having to remember them; for example C is:

TinyTone works for 1MHz or 8MHz clocks. To use it with an 8MHz clock add 3 to the prescaler, to reduce the frequency by an additional factor of 8, as shown in the commented second line of the routine.

Note that for historical reasons each octave goes from C to B; for example, the next note after B4 is C5, and so on.

Specifying the duration

Once you program Timer/Counter1 with a divisor and prescaler it continues to generate a tone on output 1 indefinitely, irrespective of what the program is doing. To create a note of a specified length we need to stop the counter when the duration has elapsed.

One approach is to get the timer to call an interrupt service routine on every cycle; we can then count how many cycles have elapsed, and stop the counter after the appropriate number. This is the approach taken by Bruce E. Hall [4] .

However, I decided it was simpler to use the delay() function to time the note; after the delay has elapsed we stop the counter by writing 0 to the prescaler register.

TinyTone program

Here’s the complete program:

Addendum

For an improved version of this routine, that can be used to play notes on the ATtiny85 pins 1 or 4, see Playing Notes on the ATtiny85.

Урок 7.1 Генерация звука при помощи AVR микроконтроллера

000001Порадовать глаз различными миганием светодиодов мы уже умеем, а почему бы нам еще и не порадовать слух. В данном уроке мы рассмотрим как сгенерировать звук при помощи таймера AVR, вывести ее на динамик, и в конце концов сделаем некоторое подобие midi плеера. Чтож за дело…

Итак для сборки понадобится Atmega8, стандартная обвязка (кварц на 8МГц, 2 конденсатора на 22пФ) и пьезоизлучатель без внутреннего генератора, например HC0903A.

Можно, конечно, взять какой нибудь динамик 8Ом, но к нему придется городить усилитель. Нам же для образовательных целей достаточно будет простейшей схемы, которая без обвязки будет выглядеть так:

0000002

Достаточно просто, поэтому сразу переходим к теории. Чтобы создать звук нам нужно заставить колебаться мембрану динамика с определенной частотой. Каждой ноте соответствует своя частота, например ноте До 1 октавы, соответствует частота 261Гц. Т.е. дрыгая ногой микроконтроллера, подключенной к динамику, со скоростью 261 раз в секунду мы будем слышать звучание этой ноты. Для тех кто не силен в музыкальной теории, звук ближе от 1кГц и выше будет более писклявый, ниже 300Гц будет басить.

Перейдем к реализации. Как заставить ногу дрыгаться с такой скоростью? В этом нам поможет таймер, работу которого мы изучили в предыдущих уроках. В данном случае, нам пригодится timer1. Принцип формирования частоты таков: таймер тикает, до тех пор пока его значение не совпадет OCR1A. В в момент совпадения OCR1A, с текущим значением таймера происходит прерывание (выполняется функция) в котором текущее состояние PORTB.3 инвертируется (включается/отключается), таким образом мы получаем «пульсирующий» сигнал(мендр). Регулируя OCR1A мы изменяем частоту. Все, никаких сложностей.

Код исполняемый в прерывании:

interrupt [TIM1_COMPA] void timer1_compa_isr(void)
<
PORTB.3=!PORTB.3;
>

Теперь, нужно соотнести каждой ноте частоты и по очереди их воспроизводить, т.е. создать массив со значениями, которые по очереди будем подставлять в OCR1A. Прошивка далеко не идеальна, но вполне работоспособна. Доступна тут.

На последок видео, подобрал первое что в голову пришло: марио и танчики)))

46 комментариев: Урок 7.1 Генерация звука при помощи AVR микроконтроллера

Добрый день. Я хотел сгенерировать звук с частотой 500 герц. Создавая проект в CV, настраиваю таймер: в clock value ставлю 8000000khz. Ставлю галочку в compare a match. И перевожу 500 в 1F4. Генерю. Пишу код PORTB.3=!PORTB.3;.А в итоге получается щелк с большой задержкой. Что делать? Больше кодов не писал.

Можете мне скинуть пример генерации одного звука?

Почитайте 5 урок, там все написано. Вам только и нужно настроить прерывание по совпадению. Не забывайте про фьюзы, если камень с завода не перепрошивался, то на нем будет 1МГц.

Дело в том что это все стимулировалось в протеусе. Я делаю все как в 5 уроке. Но результат такой и был «щелк с длинной задержкой». И нужно ли таймер нужно запускать в коде и останавливать?

про протеус можете забыть, он считает не в реальном времени, поэтому и результат соответствующий

Как реализовать звук на attiny88

Генерация звука на микроконтроллерах 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):

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

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

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

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

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

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

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

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

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

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

Немного кода

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

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

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

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

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

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

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

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

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

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

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

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

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

Музыка

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

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

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

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

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

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

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

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

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

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

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

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

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

UPD: На данный момент wavepot сильно поменялся и мой код с ним не совместим. Можно локально поднять старую версию с github, но она работает только с firefox, а в chrome не выводит звук.

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

Подключение

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

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

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

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

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

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

Здесь важно правильно смешать 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 МГц.

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

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

Из известных мне микроконтроллеров с 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 миллисекундам и это максимальный цельный промежуток времени, который можно отдать другим задачам. При этом выполнение задач все равно периодически приостанавливается прерываниями.

Подключение звукового модуля

5. Добавляем в sound.c свои мелодии и прописываем названия мелодий в массив melody[].

Добавление мелодий

генерация звука на avr. структура мелодии

Использование звукового модуля

Файлы

Примеры использования звукового модуля вы можете скачать по ссылкам ниже. Схему рисовать не стал, потому что там все просто. Пьезоизлучатель подключен к выводу PB0, кнопка запуска мелодий подключена к выводу PD3. В проектах определено 4 мелодии. Нажатие на кнопку запускает каждый раз новую мелодию. Используется микроконтроллер atmega8535. Изначально хотел заморочиться на проект с четырьмя кнопками — PLAY, STOP, PAUSE и NEXT, но потом подумал, что это лишнее.

PS: Модуль не проходил расширенное тестирование и предоставляется “как есть“. Если есть какие-то рациональные предложения — давайте его доработаем.

Related items
  • Библиотека для опроса кнопок
  • Работа с SD картой. Воспроизведение wav файла. Ч3
  • Работа с SD картой. Библиотека Petit FatFs. Ч2
  • Работа с SD картой. Подключение к микроконтроллеру. Ч1
  • AVR315: Использование TWI модуля в качестве ведущего I2C устройства
Comments

Pashgan молодца.
Так держать!
Вот еще пара мелодий:
Code:
Griboedov[] = ;

unsigned int Augustin[] = ;

/* Я проанализировал «глазками» работу программы и у меня следующие замечания

1) Самое главное — в функции SOUND_tone оператор tone = toneNote; надо заменить на оператор tone += toneNote; Первый полупериод выходного сигнала генерируется правильно, например 2,5 периода работы таймера, пауза и все последующие импульсы будут генерироваться длительностью 3 периода.

2) Второе — разрешать прерывание по компаратору в модулях SOUND_Com и SOUND_PlaySong преждевременно и вредно, лучше разрешение переместить в модуль SOUND_Duration и поставить его вместо команды

Как делать паузы между нотами?

На баскоме есть куча мелодий, переведенных с сименса. Пробовал такой макрос, чуш какая то получается.
#define C1 fn(262)
#define Cis1 fn(277)
#define D1 fn(294)
#define Dis1 fn(311)
#define E1 fn(330)
#define F1 fn(349)
#define Fis1 fn(370)
#define G1 fn(392)
#define Gis1 fn(415)
#define A1 fn(440)
#define Ais1 fn(466)
#define H1 fn(494)

#define C2 fn(523)
#define Cis2 fn(554)
#define D2 fn(587)
#define Dis2 fn(622)
#define E2 fn(659)
#define F2 fn(698)
#define Fis2 fn(740)
#define G2 fn(784)
#define Gis2 fn(831)
#define A2 fn(880)
#define Ais2 fn(932)
#define H2 fn(988)

#define C3 fn(1047)
#define Cis3 fn(1109)
#define D3 fn(1175)
#define Dis3 fn(1245)
#define E3 fn(1319)
#define F3 fn(1397)
#define Fis3 fn(1480)
#define G3 fn(1568)
#define Gis3 fn(1661)
#define A3 fn(1720)
#define Ais3 fn(1865)
#define H3 fn(1976)

Урок 7.1 Генерация звука при помощи AVR микроконтроллера

000001Порадовать глаз различными миганием светодиодов мы уже умеем, а почему бы нам еще и не порадовать слух. В данном уроке мы рассмотрим как сгенерировать звук при помощи таймера AVR, вывести ее на динамик, и в конце концов сделаем некоторое подобие midi плеера. Чтож за дело…

Итак для сборки понадобится Atmega8, стандартная обвязка (кварц на 8МГц, 2 конденсатора на 22пФ) и пьезоизлучатель без внутреннего генератора, например HC0903A.

Можно, конечно, взять какой нибудь динамик 8Ом, но к нему придется городить усилитель. Нам же для образовательных целей достаточно будет простейшей схемы, которая без обвязки будет выглядеть так:

0000002

Достаточно просто, поэтому сразу переходим к теории. Чтобы создать звук нам нужно заставить колебаться мембрану динамика с определенной частотой. Каждой ноте соответствует своя частота, например ноте До 1 октавы, соответствует частота 261Гц. Т.е. дрыгая ногой микроконтроллера, подключенной к динамику, со скоростью 261 раз в секунду мы будем слышать звучание этой ноты. Для тех кто не силен в музыкальной теории, звук ближе от 1кГц и выше будет более писклявый, ниже 300Гц будет басить.

Перейдем к реализации. Как заставить ногу дрыгаться с такой скоростью? В этом нам поможет таймер, работу которого мы изучили в предыдущих уроках. В данном случае, нам пригодится timer1. Принцип формирования частоты таков: таймер тикает, до тех пор пока его значение не совпадет OCR1A. В в момент совпадения OCR1A, с текущим значением таймера происходит прерывание (выполняется функция) в котором текущее состояние PORTB.3 инвертируется (включается/отключается), таким образом мы получаем «пульсирующий» сигнал(мендр). Регулируя OCR1A мы изменяем частоту. Все, никаких сложностей.

Код исполняемый в прерывании:

interrupt [TIM1_COMPA] void timer1_compa_isr(void)

Теперь, нужно соотнести каждой ноте частоты и по очереди их воспроизводить, т.е. создать массив со значениями, которые по очереди будем подставлять в OCR1A. Прошивка далеко не идеальна, но вполне работоспособна. Доступна тут.

На последок видео, подобрал первое что в голову пришло: марио и танчики)))

46 комментариев: Урок 7.1 Генерация звука при помощи AVR микроконтроллера

Добрый день. Я хотел сгенерировать звук с частотой 500 герц. Создавая проект в CV, настраиваю таймер: в clock value ставлю 8000000khz. Ставлю галочку в compare a match. И перевожу 500 в 1F4. Генерю. Пишу код PORTB.3=!PORTB.3;.А в итоге получается щелк с большой задержкой. Что делать? Больше кодов не писал.

Можете мне скинуть пример генерации одного звука?

Почитайте 5 урок, там все написано. Вам только и нужно настроить прерывание по совпадению. Не забывайте про фьюзы, если камень с завода не перепрошивался, то на нем будет 1МГц.

Дело в том что это все стимулировалось в протеусе. Я делаю все как в 5 уроке. Но результат такой и был «щелк с длинной задержкой». И нужно ли таймер нужно запускать в коде и останавливать?

про протеус можете забыть, он считает не в реальном времени, поэтому и результат соответствующий

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *