«Ода к радости» из урока про массивы нот играла у меня исправно ровно до вечера, когда я захотел сделать дверной звонок бодрее. Темп оказался зашит в шестнадцати числах длительностей: править их по одному — работа на вечер, а одна неверная правка — и ритм рассыпается. Второй удар был обиднее: я прикрутил кнопку, чтобы мелодии листались с двери, и она не отвечала. На время проигрыша скетч сидел в delay и молчал глухо, как рыба: фраза шла четыре секунды, и все четыре кнопка не значила ничего.
Здесь второй заход на мелодии, и он про управление, а не про теорию: ноты поднимаем на октаву выше — ля 440, до 523, ми 659, соль 784, до второй октавы 1046, на пьезо верхние звучат бодрее, — длительности пишем долями и умножаем на темп одной константой, между нотами ставим noTone(), а в конце превращаем блокирующий проигрыш в планировщик на millis(), при котором кнопка живёт каждый проход loop(). Механику tone() и noTone() разбирали в уроке про зуммер — здесь она берётся как готовый инструмент.
План такой: сначала простая версия с delay и темпом-константой, затем честный подсчёт её слепоты в миллисекундах, потом планировщик — и проверка в симуляторе, что кнопка и мелодия живут одновременно. Скетчей два, второй отличается от первого на десяток строк, но между ними вся дистанция между игрушкой и прибором.
Партитура: октава вверх и доли вместо миллисекунд
Частоты в партитуре ниже взяты не с потолка: ля 440 — эталон, по которому настраивают оркестры, до второй октавы 523, ми 659, соль 784, до третьей — 1046. Поднимать мелодии выше оригинала на пьезозуммере — приём рабочий: резонанс корпуса лежит в районе 2-4 кГц, и чем выше нота, тем ближе она к резонансу и тем громче звучит. До первой октавы на дешёвом излучателе бормочет, а 523 и 1046 — звонят в полный голос. Поэтому нотные записи я для зуммера обычно переношу на октаву вверх и не жалею.
Длительности пишу долями, а не миллисекундами, и счёт веду восьмыми: единица в массиве — восьмая нота, двойка — четверть, четвёрка — половинная. Темп — одна константа BEAT, миллисекунды на восьмую, и всё умножение происходит в скетче. Хочу медленнее — меняю одно число; хочу всю фразу вдвое короче — меняю то же число. Массивы идут парой: melody[i] — частота, beats[i] — доля той же ноты, и разъезжаться им нельзя: правлю всегда вместе, иначе мелодия заиграет чужим ритмом.
Интервалы в моей фразе не случайные: 440, 523, 659 и 784 — это до-ми-соль, то самое мажорное трезвучие, которое звучит приветливо на любом инструменте, включая пищалку. Расстояния между нотами считаются полутонами, и соседний полутон — множитель 1,059: от ля 440 три полутона вверх дают до 523, ещё четыре — ми 659, ещё три — соль 784. Хотите свою фразу — берите ноты из таблицы прошлого урока или умножайте 440 на 1,059 нужное число раз — ухо услышит фальшь так же сразу, как на гитаре.
| Доля | Число в массиве | При темпе 250 мс | Звучит как |
|---|---|---|---|
| восьмая | 1 | 250 мс | короткий шаг, держит движение фразы |
| четверть | 2 | 500 мс | базовая нота, на ней стоит ритм |
| четверть с точкой | 3 | 750 мс | песенная нота, тянет на себе такт |
| половинная | 4 | 1000 мс | финал фразы, после неё пауза честная |
const int BUZZER = 8;const int BEAT = 250; // мс на восьмую — весь темп здесь int melody[] = {440, 523, 659, 784, 0, 784, 659, 523, 1046, 0};int beats[] = {2, 2, 2, 2, 1, 2, 2, 2, 4, 2};const int N = 10; void setup() { pinMode(BUZZER, OUTPUT); Serial.begin(9600); Serial.println("Melodia: 10 not, BEAT 250");} void loop() { for (int i = 0; i < N; i = i + 1) { if (melody[i] > 0) { tone(BUZZER, melody[i]); // без третьего аргумента Serial.print("note="); Serial.println(melody[i]); } delay(beats[i] * BEAT); // нота или пауза звучит своё время noTone(BUZZER); // щель 25 мс — ноты не слипаются delay(25); } delay(2000); // тишина между повторами}Разница с прошлым уроком — в способе отбоя. Там работала связка tone(..., длительность) плюс delay с запасом; здесь я играю tone(BUZZER, частота) без третьего аргумента, выжидаю delay(beats[i] * BEAT), гашу звук noTone() руками и даю щель 25 мс тишины. На живой плате оба варианта звучат почти одинаково, но noTone-версия честнее держит границу: обрыв происходит ровно на команде, а не где-то в таймере, и поведение одинаковое в симуляторе и на железе. Щель нужна не для красоты: две одинаковые ноты подряд без неё сливаются в один длинный писк — ухо слышит одну ноту вдвое длиннее, и ритм врёт. Двадцать-тридцать миллисекунд разделяют их; больше шестидесяти — фраза становится рубленой.
Частый вопрос: как играть ноты тише? Через tone() — никак: громкость зашита в скважности ровно 50 процентов, и параметра громкости у функции просто нет. Громкость правится железом: отверстием в корпусе, резонансной частотой и питанием. Из этого следует важный вывод: динамика сигнала строится не силой звука, а высотой и ритмом — фразы, которые мы собираем в третьем уроке, именно поэтому играют изменением частоты и паузами, а не громкостью.
Теперь грабля, из-за которой звонок переписывался: delay(beats[i] * BEAT) замораживает loop() целиком. Фраза из десяти нот при темпе 250 длится (2+2+2+2+1+2+2+2+4+2)×250 плюс десять щелей по 25 — почти пять секунд, в которые digitalRead не вызывается ни разу. Кнопка, датчик, светодиод — всё стоит. Это та же болезнь, что в уроке про millis() вместо delay: блокирует не tone(), а пауза, и лечится она планировщиком, а не заменой зуммера.
Живой проигрыш: планировщик на millis
Планировщик переворачивает вопрос: не «играть ноту и спать», а «проверить, не пора ли следующую». Состояний два — номер ноты i и момент tNext, когда она должна закончиться. На каждом проходе loop() опрашивается кнопка, и если millis() доехал до tNext, текущая нота глушится noTone(), включается следующая, tNext сдвигается на её длительность. Нота играет «сама» — точнее, время отсчитывает millis(), пока скетч крутится дальше и остаётся живым.
Так устроен кооперативный цикл и весь планировщик задач на millis: мелодия становится очередным делом с расписанием, наравне с опросом кнопки и датчиков. Переход millis() через ноль на 49-е сутки здесь не страшен: разность millis() - tNext в беззнаковой арифметике остаётся корректной и через переполнение — приём разобран в уроке про планировщик. Хуже планировщика только вариант «запустить мелодию в прерывании» — таймер там общий с tone(), и я не рекомендую даже пробовать.
Мелочь, на которой стоит задержаться: темп меняется посреди ноты — новая длительность применится только к следующей, а текущая доигрывает по старому плану. Это нормальный компромисс: пересчитывать tNext на лету можно, но на слух разница не различается, а код усложняется на проверку. Заодно это бесплатная защита от дребезга: два нажатия подряд просто сменят темп дважды, и мелодия не споткнётся ни на одной ноте.
const int BUZZER = 8;const int BTN = 2;int beatMs = 250; // темп, кнопка его меняет int melody[] = {440, 523, 659, 784, 0, 784, 659, 523, 1046, 0};int beats[] = {2, 2, 2, 2, 1, 2, 2, 2, 4, 2};const int N = 10; int i = 0;unsigned long tNext = 0;int prevBtn = HIGH; void setup() { pinMode(BUZZER, OUTPUT); pinMode(BTN, INPUT_PULLUP); Serial.begin(9600); Serial.println("Melodia na millis, knopka D2: tempo");} void loop() { int btn = digitalRead(BTN); if (btn == LOW && prevBtn == HIGH) { // фронт нажатия if (beatMs == 250) { beatMs = 125; } else { beatMs = 250; } Serial.print("tempo="); Serial.println(beatMs); delay(30); } prevBtn = btn; if (millis() >= tNext) { noTone(BUZZER); int f = melody[i]; if (f > 0) { tone(BUZZER, f); Serial.print("note="); Serial.println(f); } tNext = millis() + beats[i] * beatMs + 25; i = i + 1; if (i >= N) i = 0; }}В симуляторе собрана именно millis-версия: плата подрисует зуммер на D8 и кнопку на D2, браузер заиграет ноты через динамики. Кликайте кнопку: темп падает с 250 на 125 мс на восьмую, фраза бежит вдвое быстрее, и ни одна нота при этом не теряется — короткий такт опроса в 10-30 мс ловит каждое касание. В мониторе идут пары note=440 и tempo=250: ноты ложатся на график плоттера ступеньками, а переключение темпа видно по смене плотности ступенек.
Транспонирование в такой партитуре бесплатное: удвойте каждую частоту — мелодия уедет на октаву вверх, длительности не трогаются; потолок аргумента tone() — 65535 Гц, но слышимая зона кончается на 20 кГц, выше сдвигаться незачем. Если нужны две фразы — приветствие и тревога — заводятся две пары массивов, а выбор между ними делает switch по переменной режима, как в уроке про switch-case. Новую фразу я всегда правлю, слушая целиком: опечатка в одной ноте на слух ловится мгновенно, а глазом — не всегда.
- Партитура — два массива-пары: melody[] с частотами (ля 440, до 523, ми 659, соль 784, до2 1046) и beats[] с долями; счёт в восьмых, темп — константа BEAT
- tone(BUZZER, f) без третьего аргумента, delay(доля × BEAT), noTone() и щель 20-30 мс: контролируемый обрыв и раздельные одинаковые ноты
- Темп правится одним числом; транспонирование — умножением всех частот на два вверх или делением вниз
- Мелодия на delay() глуха к кнопке: фраза на пять секунд — пять секунд без опроса; лечится планировщиком на millis()
- Millis-проигрыватель держит два состояния — номер ноты и tNext — и опрашивает кнопку каждый проход loop()
- 1. В массиве мелодии стоит ноль. Что делает скетч из урока?
- 2. Нота записана числом 2, темп 250 мс. Сколько она прозвучит?
- 3. Зачем между нотами ставят щель 20-30 мс тишины?
- 4. Почему мелодия на delay() и кнопка не живут вместе?
- 5. Что даёт millis-планировщик мелодии?
Частые вопросы по теме урока
- В массиве мелодии стоит ноль. Что делает скетч из урока?
- Частоты 0 не существует — это маркер тишины. Условие if (melody[i] > 0) пропускает tone(), а delay выдерживает паузу как обычную ноту. ничего: ноль — маркер паузы, tone() пропущен
- Нота записана числом 2, темп 250 мс. Сколько она прозвучит?
- Счёт ведётся восьмыми: двойка — четверть, и при темпе 250 мс на восьмую она звучит 2 × 250 = 500 мс. 500 мс
- Зачем между нотами ставят щель 20-30 мс тишины?
- Без щели две ноты 440 Гц подряд слышатся одной удвоенной длины, и ритм врёт. 20-30 мс разделяют их, а перебор выше 60 мс делает фразу рубленой. одинаковые соседние ноты без щели сливаются в один длинный тон
- Почему мелодия на delay() и кнопка не живут вместе?
- Блокирует не tone(), а delay: на время паузы loop() стоит, и digitalRead не вызывается. Лечение — millis-планировщик, где нота запланирована на момент в будущем. на время delay() скетч не опрашивает кнопку: фраза на пять секунд — пять секунд слепоты
- Что даёт millis-планировщик мелодии?
- Состояний всего два — номер ноты и tNext. loop() крутится без длинных пауз, поэтому всё остальное хозяйство скетча продолжает жить. кнопка и датчики опрашиваются каждый проход loop(), а нота просто запланирована на момент tNext