Бурбулилка, или много болтовни около трёх строчек кода
Давным-давно, когда я был совсем маленьким, я купил билет на московский международный ки журнал Радио. И со скуки сделал фиготу, которая там была нарисована. Сейчас поискал, думал вспомню, какая именно фигота это была — но увы мне, не смог вспомнить. Может быть фигота под названием «Мелодичный автомат»… Главное, что запомнил суть — берём поток случайных чисел, и запихиваем в звучалку. Тогда я посидел вечерок с макеткой, спаял, включил и прикололся со звука, который оно издавало.
А теперь вспомнил, реализовал то же в софте, и решил приколоть коллег. Заодно и вам покажу. Сделать что-ли типа мини-туториал, чисто ради графомании? Пусть будет так.
Итак, чтобы сделать бурбулятор, нам потребуется источник интервала, источник псевдослучайного потока, и звучалка.
Начнём с источника интервала — это стандартная штуковина, которая присутствует почти во всех проектах. Везде, где приходится регулярно совершать действия — например, опрашивать периферию или крутить приглашение на экранчике, или определять факт наступления таймаута — кругом нужен источник времени. Интервал, который он формирует, может быть любым удобным, я как-то больше привык к 10ms. Шестнадцатибитный таймер идеально подходит, потому что легче попасть в целевое значение. Восьмибитный таймер, к примеру, может дать интервал длительностью или 9.98 миллисекунд, или 10.04 миллисекунды, вариант 10.00 не получается, потому как у таймера почему-то не бывает дробных значений делителя. Впрочем, если не кратный секунде интервал допустим, то почему бы и не использовать 9.98ms? В большинстве случаев это не важно.
Но пока мы заводим шетнадцатибитный таймер 1. Всё что для этого нужно — очистить счётчик, комбинацией бит в управляющих регистрах подключить источник тактирования, выбрать режим CTC, и задать граничное значение, до которого таймер будет считать.
Сразу после этого тамер начнёт счёт — каждые 256 тиков системного генератора содержимое счётчика таймера увеличится на 1. И как только оно станет равным граничному — установится бит OCF, счётчик сбросится в ноль, и начнёт считать снова. Заглянув во флаг — всегда можно увидеть, завершился интервал или нет. Конечно, такой подход порождает некий неопределённый временной промежуток между установкой флага и обнаружением флага программой. Но в подобных приложениях это несущественно, тем более что эта ошибка не накапливается, максимум что может произойти — пропуск события при серьёзной нагрузке. Но тогда надо менять или алгоритм, или контроллер — кто-то из них неадекватен задаче.
Так выглядит код, проверяющий срабатывание интервального таймера.
То есть проверяем флаг, и если он не установлен, возвращаемся к вызвавшей программе. А если установлен — сбрасываем, и исполняем разные действия, в ознаменование завершённого десятимиллисекундного интервала. В нашем случае, отсчитываем длительность каждой из нот бурбуления.
Это тоже просто и стандартно — на каждое срабатывание таймера уменьшаем переменную на единицу. Как только переменная обнулится — заново инициализируем её значением длительности, и меняем ноту. И вот таких реакций на завершение определённых интервалов — их можно наделать великое множество, пока будет хватать производительности контроллера.
Вторая часть Марлезонского балета — генератор случайных чисел. Самый простой, надёжный и правильный генератор случайных чисел делается на базе полинома. Это какбы аксиома. Ещё бывают всякие шипелки на базе стабилитронов, но, положа руку на сердце — всё, на что они годятся — это генерация зерна (seed value) для полиномальных генераторов. Ну да, последовательность, которую делает полиномальный генератор не является случайной в прямом смысле этого слова — последовательность всегда одна и та же. Но грамотный подбор полинома позволяет сделать её очень-очень длинной, а встряхивание зерен начинает начать с труднопрогнозируемого места. Впрочем, про это написано много книг, которым не место в рассказе о бурбулилке. Хочу сказать, что главное достоинство полинмальных генераторов — это равномерное распределение генерируемых чисел по диапазону.
Та же шипелка на стабилитроне не может этим похвасаться — даже при идеальной настройке усилителя либо не будут достигаться граничные числа диапазона, либо они будут выпадать чаще из-за насыщения измерителя.
А тут вот и полином под рукой завалялся — генератор CRC для далласовских микросхем. Наша процедура получения случайных чисел будет просто подсчётом CRC для последовательности 0,1,2,3,4…
Теперь у нас есть типаслучайное число в диапазоне 0-255, и мы будем превращать его в ноту. Допустим, в любую из нот двух октав, итого 14 нот. Всё, что нам нужно, чтобы привести число из диапазона 0-255 к диапазону 0-13, это умножить его на 14/256. То есть просто умножаем на 14, и игнорируем младший байт результата.
И дальше используем это число в качестве индекса к массиву нот.
Массив нот — это набор констант, делителей для таймера.
Теперь звучим: Таймер настраиваем в уже знакомый нам режим CTC, то есть счёта до граничного значения. Только тепереь вкючаем ещё и режим переключения ножки OC0B на граничном значении. То есть таймер дотикает до определённой нми границы, сам переключит состояние ножки D5, сбросится и пойдет считать снова. Потом опять досчитает и снова переключит, так на этом выводе сформируются импульсы нужной нам частоты.
Ну и всё. Теперь всё, что требуется — при пуске завести источник интервала, а потом бесконечно терпеливо вызывать провеку интервала. Остальное отработает какбы само. А вместе с проверкой интервала можно вызывать ещё много всякого интересного. И в результате контроллер будет заниматься всякими нужными делами, и при этом бурбулить.
Возьмите плату от ардуины, подключите к ножке D5 пищалку от материнской платы через резистор 200ом, чтобы не отжечь порт, влейте прошивку, и случится вам щасте — услышите бурбуление.
А теперь вспомнил, реализовал то же в софте, и решил приколоть коллег. Заодно и вам покажу. Сделать что-ли типа мини-туториал, чисто ради графомании? Пусть будет так.
Итак, чтобы сделать бурбулятор, нам потребуется источник интервала, источник псевдослучайного потока, и звучалка.
Начнём с источника интервала — это стандартная штуковина, которая присутствует почти во всех проектах. Везде, где приходится регулярно совершать действия — например, опрашивать периферию или крутить приглашение на экранчике, или определять факт наступления таймаута — кругом нужен источник времени. Интервал, который он формирует, может быть любым удобным, я как-то больше привык к 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
Ну вот, самого интересного — аудио или видео с бурбулением — нету.
- Melted_Metal
- 07 мая 2018, 20:25
- ↓
Это если вы можете прошить МК, спаять по-бырому схемку и запустить бурбулилку, а если нет?
- Melted_Metal
- 09 мая 2018, 13:03
- ↑
- ↓
Еще быстрее сложить руки рупором и побурбулить голосом)) И без МК и программирования это будет!
- Melted_Metal
- 09 мая 2018, 15:49
- ↑
- ↓
Ну если ты можешь рандомно пищать чистым меандром — то почему бы и нет. Но у людей обычно проблемы еще на рандоме начинаются.
Меандр все равно сгладится индуктивностью динамика, а рандом, да, проблема.
- Melted_Metal
- 09 мая 2018, 15:53
- ↑
- ↓
ВОт, пжалста, десять строчек:
Можно просто запустить в консоли «tcc -run -», вставить текст (после вставки добавить Ctrl-Z Enter) и готово!
Правда, на реалтековском имитаторе писиськера тормозит безбожно) На нормальном, аппаратном писиськере — без нареканий.
#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) и готово!
Правда, на реалтековском имитаторе писиськера тормозит безбожно) На нормальном, аппаратном писиськере — без нареканий.
Подпаяй проводки к спикеру и в звуковую карту. Будь мужиком блеать!
- coredumped
- 10 мая 2018, 08:49
- ↑
- ↓
А вот у меня не получается и пишет такое:
«tcc -run -» не является внутренней или внешней
командой, исполняемой программой или пакетным файлом.
«tcc -run -» не является внутренней или внешней
командой, исполняемой программой или пакетным файлом.
Играет, но длительности нот не музыкальные!
после вставки добавить:
Enter
Ctrl-Z
Enter
#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
Окай, вот вариант, генерирующий WAV в STDOUT. Уже не десять строчек, зато теоретически должно быть кроссплатформенно (в никсах может потребоваться выкинуть вызов setmode).
WAV «почти стандартный», некоторые программы не одобряют нулевую длину в хедере. Но ffmpeg/ffplay/WinAmp/DShow понимают.
В консоли можно насладиться писком сомнительной мелодичности командой «tcc -run bulbul.c | ffplay -» или «tcc -run — | ffplay -», в этом случае код надо скопипастить в консоль после вызова команды и завершить Ctrl-Z Enter.
Ну и сэмпл, для тех, кому лень скачать компилятор, но не лень посоветовать мне паять проводок в работающем компьютере.
#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.
Ну и сэмпл, для тех, кому лень скачать компилятор, но не лень посоветовать мне паять проводок в работающем компьютере.
Вообще-то не нужно заглядывать во флаг, а нужно применять прерывание от таймера!
И еще, в двух октавах находится не 14 нот, а их там аж 24!
И еще, в двух октавах находится не 14 нот, а их там аж 24!
Нот всегда 7, просто для минора и мажора введены 5 полутонов.
Компания нот для мажорного настроения, отсчитывая от первой ступени, подбирается по формуле: «2 тона — полутон — 3 тона — полутон».
Минорная компания, считая с первой ступени: «тон — полутон — два тона — полутон — два тона».
Компания нот для мажорного настроения, отсчитывая от первой ступени, подбирается по формуле: «2 тона — полутон — 3 тона — полутон».
Минорная компания, считая с первой ступени: «тон — полутон — два тона — полутон — два тона».
Судя по коду, контроллер AVR, скорее всего mega88 c тактированием от кварца 16МГц
- coredumped
- 10 мая 2018, 09:10
- ↑
- ↓
А забавно звучит.
К сожалению, команда вида
не работает так, как хотелось бы. У кого-то есть идеи, как это корректно заэскейпить?
К сожалению, команда вида
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 -
не работает так, как хотелось бы. У кого-то есть идеи, как это корректно заэскейпить?
Я не пользовался tcc, так что конкретно за него не скажу. А так просто минус без имени параметра обычно трактуется как «брать данные из входного потока». Только надо следить, что бы все пробелы были на месте, в частности минус обязательно отбивается пробелами с обеих сторон.
Комментарии (57)
RSS свернуть / развернуть