Бурбулилка, или много болтовни около трёх строчек кода

Давным-давно, когда я был совсем маленьким, я купил билет на московский международный ки журнал Радио. И со скуки сделал фиготу, которая там была нарисована. Сейчас поискал, думал вспомню, какая именно фигота это была — но увы мне, не смог вспомнить. Может быть фигота под названием «Мелодичный автомат»… Главное, что запомнил суть — берём поток случайных чисел, и запихиваем в звучалку. Тогда я посидел вечерок с макеткой, спаял, включил и прикололся со звука, который оно издавало.
А теперь вспомнил, реализовал то же в софте, и решил приколоть коллег. Заодно и вам покажу. Сделать что-ли типа мини-туториал, чисто ради графомании? Пусть будет так.

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

Начнём с источника интервала — это стандартная штуковина, которая присутствует почти во всех проектах. Везде, где приходится регулярно совершать действия — например, опрашивать периферию или крутить приглашение на экранчике, или определять факт наступления таймаута — кругом нужен источник времени. Интервал, который он формирует, может быть любым удобным, я как-то больше привык к 10ms. Шестнадцатибитный таймер идеально подходит, потому что легче попасть в целевое значение. Восьмибитный таймер, к примеру, может дать интервал длительностью или 9.98 миллисекунд, или 10.04 миллисекунды, вариант 10.00 не получается, потому как у таймера почему-то не бывает дробных значений делителя. Впрочем, если не кратный секунде интервал допустим, то почему бы и не использовать 9.98ms? В большинстве случаев это не важно.

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

		clr	r16
		sts	TCNT1H,r16
		sts	TCNT1L,r16
		sts 	TCCR1A,r16
		outi1	TCCR1B,(1<<WGM12|1<<CS12)		; CTC(ocr1a), prescaler=1/256
		ldi	r16,low (TimeDivider)
		ldi	r17,high(TimeDivider)
		sts	OCR1AH,r17
		sts	OCR1AL,r16


Сразу после этого тамер начнёт счёт — каждые 256 тиков системного генератора содержимое счётчика таймера увеличится на 1. И как только оно станет равным граничному — установится бит OCF, счётчик сбросится в ноль, и начнёт считать снова. Заглянув во флаг — всегда можно увидеть, завершился интервал или нет. Конечно, такой подход порождает некий неопределённый временной промежуток между установкой флага и обнаружением флага программой. Но в подобных приложениях это несущественно, тем более что эта ошибка не накапливается, максимум что может произойти — пропуск события при серьёзной нагрузке. Но тогда надо менять или алгоритм, или контроллер — кто-то из них неадекватен задаче.

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

imerTick:	sbis	TIFR1,OCF1A
		ret
		ldi	r16,(1<<OCF1A)		; Сброс события таймера
		out	TIFR1,r16


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


		lds	r16, PeriodCnt
		dec	r16
		sts	PeriodCnt,r16
		brne	_ttexit
		ldi	r16,Period
		sts	PeriodCnt,r16
		rcall	Bulbulbul
_ttexit:	ret


Вторая часть Марлезонского балета — генератор случайных чисел. Самый простой, надёжный и правильный генератор случайных чисел делается на базе полинома. Это какбы аксиома. Ещё бывают всякие шипелки на базе стабилитронов, но, положа руку на сердце — всё, на что они годятся — это генерация зерна (seed value) для полиномальных генераторов. Ну да, последовательность, которую делает полиномальный генератор не является случайной в прямом смысле этого слова — последовательность всегда одна и та же. Но грамотный подбор полинома позволяет сделать её очень-очень длинной, а встряхивание зерен начинает начать с труднопрогнозируемого места. Впрочем, про это написано много книг, которым не место в рассказе о бурбулилке. Хочу сказать, что главное достоинство полинмальных генераторов — это равномерное распределение генерируемых чисел по диапазону.
Та же шипелка на стабилитроне не может этим похвасаться — даже при идеальной настройке усилителя либо не будут достигаться граничные числа диапазона, либо они будут выпадать чаще из-за насыщения измерителя.
А тут вот и полином под рукой завалялся — генератор CRC для далласовских микросхем. Наша процедура получения случайных чисел будет просто подсчётом CRC для последовательности 0,1,2,3,4…

		lds	r16, counter
		inc	r16
		sts	counter,r16
		rcall	DowCRC


Теперь у нас есть типаслучайное число в диапазоне 0-255, и мы будем превращать его в ноту. Допустим, в любую из нот двух октав, итого 14 нот. Всё, что нам нужно, чтобы привести число из диапазона 0-255 к диапазону 0-13, это умножить его на 14/256. То есть просто умножаем на 14, и игнорируем младший байт результата.

		ldi	r16,14		
		mul	r16,r18
		mov	r16,r1


И дальше используем это число в качестве индекса к массиву нот.


		ldi	zh,high(notetable<<1)
		ldi	zl,low(notetable<<1)
		ldi	r17,0
		add	zl,r16
		adc	zh,r17
		lpm	r0,z


Массив нот — это набор констант, делителей для таймера.
Теперь звучим: Таймер настраиваем в уже знакомый нам режим CTC, то есть счёта до граничного значения. Только тепереь вкючаем ещё и режим переключения ножки OC0B на граничном значении. То есть таймер дотикает до определённой нми границы, сам переключит состояние ножки D5, сбросится и пойдет считать снова. Потом опять досчитает и снова переключит, так на этом выводе сформируются импульсы нужной нам частоты.

outi	tccr0a,(1<<COM0B0|1<<WGM01)	; (Toggle OC0B on Compare Match)(CTC mode)
		outi	tccr0b,(1<<CS02)		; div 256
		outi	OCR0B,0
		out	OCR0A,r0



Ну и всё. Теперь всё, что требуется — при пуске завести источник интервала, а потом бесконечно терпеливо вызывать провеку интервала. Остальное отработает какбы само. А вместе с проверкой интервала можно вызывать ещё много всякого интересного. И в результате контроллер будет заниматься всякими нужными делами, и при этом бурбулить.
Возьмите плату от ардуины, подключите к ножке D5 пищалку от материнской платы через резистор 200ом, чтобы не отжечь порт, влейте прошивку, и случится вам щасте — услышите бурбуление.
  • +3
  • 07 мая 2018, 18:41
  • Gornist
  • 1
Файлы в топике: BURBULIS.zip

Комментарии (57)

RSS свернуть / развернуть
Ну вот, самого интересного — аудио или видео с бурбулением — нету.
+4
Это чтобы интригу создать.
0
0
(Ничего личного, просто шутка :)
— Знаешь как заинтересовать идиота?
— Как?
— Завтра расскажу.
+2
Так иначе было бы не интересно.
0
Это если вы можете прошить МК, спаять по-бырому схемку и запустить бурбулилку, а если нет?
0
Можно написать по-бырому программку, которая будет делать все то же самое в PC-speaker.
0
Еще быстрее сложить руки рупором и побурбулить голосом)) И без МК и программирования это будет!
+1
Ну если ты можешь рандомно пищать чистым меандром — то почему бы и нет. Но у людей обычно проблемы еще на рандоме начинаются.
0
Меандр все равно сгладится индуктивностью динамика, а рандом, да, проблема.
0
ВОт, пжалста, десять строчек:
#include <Windows.h>

const int Notes[] = {262,  277,  294,  311,  330,  349,  370,  392,  415,  440, 466,  494, 523,  554,  587,  622,  659,  698,  740,  784,  830,  880,  932,  988};

int main(void) {
  srand(GetTickCount());
  for(;;)
    Beep(Notes[rand() % 24], 200 + rand() % 301);
  return 0;
}

Можно просто запустить в консоли «tcc -run -», вставить текст (после вставки добавить Ctrl-Z Enter) и готово!
Правда, на реалтековском имитаторе писиськера тормозит безбожно) На нормальном, аппаратном писиськере — без нареканий.
0
Ну хоть вы демонстрацию опубликуйте, что ли.
+1
А не проще запустить? Компилятор менее метра весит.
-1
Нет, не проще))
0
Мне нечем писать звук с писиськера, а программно его захватить нельзя)
0
Ну прям нисудьба послушать))
0
Подпаяй проводки к спикеру и в звуковую карту. Будь мужиком блеать!
0
Нет.
0
Пробовал запустить на ноутбуке под Win7 x64. Не играет.
0
Поковыряй настройки звуковой карты, возможно найдешь там опцию эмуляции PC-speaker'а. Правда, у меня оная работает на редкость слоупочно.
0
А вот у меня не получается и пишет такое:

«tcc -run -» не является внутренней или внешней
командой, исполняемой программой или пакетным файлом.
0
Я же дал ссылку на компилятор! Распакуй и выполняй команду там, куда распаковал!
0
Теперь выдает это:

<stdin>:6: warning: implicit declaration of function 'srand'

Ну и не играет конечно ((
0
Ура!!!
Заиграло!!!
0
Играет, но длительности нот не музыкальные!
#include <Windows.h>

const int Notes[] = {262,  277,  294,  311,  330,  349,  370,  392,  415,  440, 466,  494, 523,  554,  587,  622,  659,  698,  740,  784,  830,  880,  932,  988};

const int Duration[] = {1, 2, 4, 8, 16, 32, 64};

int main(void) {
  srand(GetTickCount());
  for(;;)
    Beep(Notes[rand() % 24], 1000/Duration[rand() % 7]);
  return 0;
}

после вставки добавить:
Enter
Ctrl-Z
Enter
+1
Ну вот и получилась настоящая шарманка!!!
0
Нет, больше похоже на волынку!!!
0
Вот и прекрасно. Теперь можешь припаять проводок с писиськера на звуковую карту! :D
Ну или можно переделать на генерацию .wav-файла с выводом в stdout, откуда его можно потом хоть в файл, хоть в ffmpeg отправить.
0
У меня почему-то не выдает такого. Возможно мой вариант винапи где-то включает хедер с srand (у меня полный винапи от мингва, а не обрезок из комплекта TCC).
По хорошему надо добавить первой строчкой "#include <stdlib.h>", да.
Длительности мне лень было гуглить)
0
Если добавить #include <stdlib.h> то вонинг пропадает!
0
Окай, вот вариант, генерирующий WAV в STDOUT. Уже не десять строчек, зато теоретически должно быть кроссплатформенно (в никсах может потребоваться выкинуть вызов setmode).
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <math.h>
#include <fcntl.h>

const uint8_t WAVEhdr[] = {
  0x52, 0x49, 0x46, 0x46, 0x24, 0x00, 0x00, 0x00, 0x57, 0x41, 0x56, 0x45,
  0x66, 0x6d, 0x74, 0x20, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
  0x44, 0xac, 0x00, 0x00, 0x88, 0x58, 0x01, 0x00, 0x02, 0x00, 0x10, 0x00,
  0x64, 0x61, 0x74, 0x61, 0x00, 0x00, 0x00, 0x00
};

const float dbpi = 6.2831853;

void beep(int Freq, int Dur) {
  static float Phase = 0.0;
  Phase = Phase - dbpi * floor(Phase / dbpi);
  float dPhi = dbpi * Freq / 44100.0;
  for(int i = 0; i < (44100l * Dur) / 1000; i++)
  {
    int16_t S = (int)(32767.0 * sin(Phase += dPhi));
    fwrite(&S, 2, 1, stdout);
  }
}

const int Notes[] = {262,  277,  294,  311,  330,  349,  370,  392,  415,  440, 466,  494, 523,  554,  587,  622,  659,  698,  740,  784,  830,  880,  932,  988};

const int Duration[] = {1, 2, 4, 8, 16, 32};

int main(void) {
  srand(time(0));
  setmode(fileno(stdout), O_BINARY);
  fwrite(WAVEhdr, sizeof(WAVEhdr), 1, stdout);
  for(;;)
    beep(Notes[rand() % 24], 1000/Duration[rand() % 6]);
  return 0;
}

WAV «почти стандартный», некоторые программы не одобряют нулевую длину в хедере. Но ffmpeg/ffplay/WinAmp/DShow понимают.
В консоли можно насладиться писком сомнительной мелодичности командой «tcc -run bulbul.c | ffplay -» или «tcc -run — | ffplay -», в этом случае код надо скопипастить в консоль после вызова команды и завершить Ctrl-Z Enter.
Ну и сэмпл, для тех, кому лень скачать компилятор, но не лень посоветовать мне паять проводок в работающем компьютере.
0
Добрый парсер всегда рад помочь.
tcc -run bulbul.c | ffplay -
tcc -run - | ffplay -
tcc -run - > test.wav
0
Нет, нет, синус звучит очень плохо в плане музыкальности. Меандр зазвучит намного лучше, намного приятнее!!!
+1
Все-то им не так! Кстати, вроде как раз нечетные гармоники считаются некошерными, не?
Меандр зазвучит намного лучше, намного приятнее!!!
Меандр — для счастливых обладателей правильного писиськера!
0
P.S. Как по мне — от меандра оно лучше не становится. От добавления четных гармоник — чуть помягче, но по прежнему уныло.
Впрочем, каждый желающий может пофиксить формулу генерации сигнала на свой вкус.
0
1) Что такое ffplay?
2) Попробовал проделать все это и не получается, test.wav не создается(
0
1) Консольный плеер из проекта ffmpeg. Отлично гуглится, кстати, первый же линк на официальную документацию.
2) А в консоль-то оно срет мусором если просто запустить скомпилированный файл? И он, собсна, компилируется?
0
Вообще-то не нужно заглядывать во флаг, а нужно применять прерывание от таймера!
И еще, в двух октавах находится не 14 нот, а их там аж 24!
0
Будь мужиком, не пользуй прерывание!
0
Будь всегда первым, используй только белые клавиши!
0
ЖЕЛАЮ ВСЕМ:
Во всём быть 1-ым,
Всегда иметь 2-ую половинку,
Никогда не быть 3-им лишним,
Иметь свои 4 уголка,
Что бы всё в жизни было на 5,
Иметь 6-ое чувство
И быть на 7-ом небе от счастья!!!
0
Нот всегда 7, просто для минора и мажора введены 5 полутонов.
Компания нот для мажорного настроения, отсчитывая от первой ступени, подбирается по формуле: «2 тона — полутон — 3 тона — полутон».
Минорная компания, считая с первой ступени: «тон — полутон — два тона — полутон — два тона».
0
Что такое первая ступень?
0
Завит от того в какой тональности вы играете.
Ступени гаммы ДО мажор: ДО — I, РЕ — II, МИ — III, ФА — IV, СОЛЬ — V, ЛЯ — VI, СИ — VII, ДО — снова I.
0
Хорошо! Тогда какова компания нот, например, для ля-минор?
0
С ля начинается, и ей же заканчивется. Меняется только тональность, формулы тонов и полутонов сохраняются.
0
а какая именно плата ардуино применяется? или проще какой контроллер?
0
Судя по коду, контроллер AVR, скорее всего mega88 c тактированием от кварца 16МГц
0
мега 328
0
Вот ещё замечательный бурбулятор — раз ссылка, два ссылка.
+1
А забавно звучит.
К сожалению, команда вида
echo main(t){setmode(1,32768);for(t=0;;t++)putchar(t*(((t>>12)|(t>>8))&(63&(t>>4))));} | tcc -run - | ffplay -u s8 -ar 8k -ac 1 -

не работает так, как хотелось бы. У кого-то есть идеи, как это корректно заэскейпить?
0
для начала тело программы стоит взять в кавычки
0
Это помогает запихнуть программу в пайп, но тогда tcc ее не понимает…
0
Я не пользовался tcc, так что конкретно за него не скажу. А так просто минус без имени параметра обычно трактуется как «брать данные из входного потока». Только надо следить, что бы все пробелы были на месте, в частности минус обязательно отбивается пробелами с обеих сторон.
0
Все так. Проблема в том, что echo выводит строку так, как получил. Вместе с кавычками. И tcc удивляется, получив на вход строковый литерал и ничего более:
stdin:1: error: declaration expected
0
Тогда просто заискейпить все, кроме букв и цифр.
0
Вот я и не помню, как эскейпить в винде…
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.