Музыка в Arduino может быть фоновой. У меня появилась идея написать новую музыкальную программу или библиотеку для музыкальной программы.

Идеи появляются не на пустом месте. Первый музыкальный автомат я собрал 30 лет назад, в нём был генератор, подстроечные резисторы и дешифратор. Недостатки этой схемы побудили меня разработать свою схему и тогда я разработал цифровой таймер-счётчик на D-триггерах, управляемый по программе из прожигаемого ПЗУ. Последние 8 лет я программирую микроконтроллеры. Первый мой музыкальный автомат был разработан на Ассемблере для м-к ATtiny13. Тогда, почему-то, захотелось написать генератор нот без использования таймеров-счётчиков м-к. И это получилось! Писал для Arduino с использованием таймеров-счётчиков м-к. Это просто. Недавно я закончил серьёзный проект на ATmega328. В этом проекте задачи добавлялись несколько раз так, что в конце, чтобы всё заработало, пришлось оптимизировать программу по объёму, и главное по использованию памяти, переписать насколько библиотек и разработать 2 свои. Теперь я более оптимистично смотрю на ресурсы Ардуино.

Сегодня появилась идея к которой не приступишь в лоб. Нужна концепция. Вот об этом и буду писать.

  1. Музыка в Arduino может быть фоновой, она может звучать в то время как выполняется основная программа. Должна быть многозадачность. Значит надо будет использовать прерывания.
  2. Одного байта достаточно чтобы хранить код ноты и её длительность. Отведём 3 бита под длительности 1, 1/2, 1/4, 1/8, 1/16, 1/32, 1/64, "Конец". 5 бит под коды нот (31 нота + пауза). Номер паузы 0.
  3. Так как массив нот будет храниться в шифрованном виде, готовить этот массив надо будет во вспомогательной программе, например в Open Office.
  4. Так как оперативной памяти в м-к мало, коды нот будем хранить в Flash памяти программ м-к.
  5. Так как музыкальное произведение чаще всего состоит из повторяющихся фрагментов, в программе должны быть предусмотрены вызовы этих фрагментов.
  6. В программе должны быть предусмотрены команды управления воспроизведением (вперёд, назад, стоп, по номеру, повтор, повтор всего).

Кажется, с концепцией определились.

Возможная техническая реализация.

Длительность ноты.

Пусть WDT (Watchdog) отвечает за длительность ноты. Распишем коды длительностей:

Длительность Сек Команда Код команды Код 3 бита  Инверсный код
1/64 0.015625 WDTCSR = (1 << WDIE ); 0b01000000 000 7
1/32  0 .03125 WDTCSR = (1 << WDP0) | (1 << WDIE ); 0b01000001 001  6
1/16  0.0625 WDTCSR = (1 << WDP1) | (1 << WDIE ); 0b01000010 010  5
1/8  0.125  WDTCSR = (1 << WDP0) | (1 << WDP1) | (1 << WDIE ); 0b01000011 011  4
1/4  0.25  WDTCSR = (1 << WDP2) | (1 << WDIE ); 0b01000100 100  3
1/2  0.5  WDTCSR = (1 << WDP0) | (1 << WDP2) | (1 << WDIE ); 0b01000101 101  2
1 WDTCSR = (1 << WDP1) | (1 << WDP2) | (1 << WDIE ); 0b01000110 110  1
Конец       111  0

Табл. 1. Коды длительностей нот.

; ***** WATCHDOG *********************
; WDTCSR - Watchdog Timer Control Register
.equ	WDP0	= 0	; Watch Dog Timer Prescaler bit 0
.equ	WDP1	= 1	; Watch Dog Timer Prescaler bit 1
.equ	WDP2	= 2	; Watch Dog Timer Prescaler bit 2
.equ	WDE	= 3	; Watch Dog Enable
.equ	WDCE	= 4	; Watchdog Change Enable
.equ	WDP3	= 5	; Watchdog Timer Prescaler Bit 3
.equ	WDIE	= 6	; Watchdog Timeout Interrupt Enable
.equ	WDIF	= 7	; Watchdog Timeout Interrupt Flag

Лист. 1. Биты регистра WDTCSR микроконтроллера ATmega328

Частота ноты.

Пусть 16-ти битный таймер отвечает за  частоту ноты (высоту тона).

В м-к ATmega328 один 16-ти битный таймер-счётчик - это "Timer/Counter1". Timer/Counter1 (ТС1) может работать в режиме генератора частоты.

Таймер-счётчик ТС1 микроконтроллера, работая в режиме СТС (сравнение и сброс таймера при совпадении, один из четырёх режимов), ведёт подсчёт тактовых импульсов до совпадения с числом, заданным программистом в любом из регистров совпадения(OCR1A или OCR1B). В случае совпадения, сигнал, на выходе счётчика меняется на противоположный и счёт начинается сначала. Таймер/счётчик TC1 имеет выходы на порты микроконтроллера PB1 и PB2 (9 и 10 pin Arduino или 15 и 16 pin м-к ATmega328-PU). Таймер-счётчик приводятся в действие от системного тактового генератора, работающего на частоте 16 МГц. Если записать в регистр совпадения 0, Таймер-счётчик делит частоту на входе на 2. Если мы хотим получить с помощью таймера-счётчика, заданную частоту, мы должны записать в 16-ти битный регистр совпадения (OCR1A или OCR1B) коэффициент деления, который можно вычислить по формуле, Формуле. 1.

Формула. 1. Коэффициент деления ТС1,

В datashit на м-к ATmega328 в формуле 1 записано (1+k), но это в нашей задаче не принципиально.

Частота любой музыкальной ноты F может быть вычислена по формуле Ф.2, где N порядковый номер ноты.

Формула. 2. Частота любой музыкальной ноты.

Нота ЛЯ первой октавы имеет порядковый номер 48 и её частота, вычисленная по этой формуле равна 440 Гц. В октаве 7 нот (ДО, РЕ, МИ, ФА, СОЛЬ, ЛЯ, СИ), однако, они распределены по частоте неравномерно. На фортепианной клавиатуре вы видели что есть белые и чёрные клавиши. Белые клавиши мы уже перечислили, это основные ноты. Чёрные клавиши дополняют белые так, что все ноты в октаве, а их теперь 12, распределяются по частоте равномерно, следуя математическому закону по Формуле.2.

Перечислим все 12 нот одной октавы: ДО ДО# РЕ РЕ# МИ ФА ФА# СОЛЬ СОЛЬ# ЛЯ ЛЯ# СИ Знак # читается как диез и ставится на нотной линейке перед нотой или в начале строки. Отменяется знаком бекар ????. Теперь должно быть понятно, что нота ЛЯ второй октавы имеет номер 60 и её частота 880 Гц. Очевидно, что частота двух одноименных нот из соседних октав отличается в ровно 2 раза. Эти ноты воспринимаются на слух, как очень похожие, только одна выше другой. В программе на языке Си для Ардуино, чтобы извлечь желаемую ноту, мы должны будем записать (присвоить) в 16-ти битный регистр совпадения (OCR1A или OCR1B) коэффициент деления вычисленный для этой ноты.

Для автоматизации расчёта коэффициентов деления для всех нот мы воспользовались электронной таблицей Google документы (Можно использовать LibreOffice Calc). В результате получилась таблица табл. 2.

  A B C D E F
1 N Октава Нота F Gz К дел.
2 1 27 0 До 131 61156
3 2 28 0 До# 139 57724
4 3 29 0 Ре 147 54484
5 4 30 0 Ре# 156 51426
6 5 31 0 Ми 165 48540
7 6 32 0 Фа 175 45815
8 7 33 0 Фа# 185 43244
9 8 34 0 Соль 196 40817
10 9 35 0 Соль# 208 38526
11 10 36 0 Ля 220 36364
12 11 37 0 Ля# 233 34323
13 12 38 0 Си 247 32396
14 13 39 1 До 262 30578
15 14 40 1 До# 277 28862
16 15 41 1 Ре 294 27242
17 16 42 1 Ре# 311 25713
18 17 43 1 Ми 330 24270
19 18 44 1 Фа 349 22908
20 19 45 1 Фа# 370 21622
21 20 46 1 Соль 392 20408
22 21 47 1 Соль# 415 19263
23 22 48 1 Ля 440 18182
24 23 49 1 Ля# 466 17161
25 24 50 1 Си 494 16198
26 25 51 2 До 523 15289
27 26 52 2 До# 554 14431
28 27 53 2 Ре 587 13621
29 28 54 2 Ре# 622 12856
30 29 55 2 Ми 659 12135
31 30 56 2 Фа 698 11454
32 31 57 2 Фа# 740 10811
33 32 58 2 Соль 784 10204
34 33 59 2 Соль# 831 9631
35 34 60 2 Ля 880 9091
36 35 61 2 Ля# 932 8581
37 36 62 2 Си 988 8099
38 37 63 3 До 1047 7645
39 38 64 3 До# 1109 7215
40 39 65 3 Ре 1175 6810
41 40 66 3 Ре# 1245 6428
42 41 67 3 Ми 1319 6067
43 42 68 3 Фа 1397 5727
44 43 69 3 Фа# 1480 5405
45 44 70 3 Соль 1568 5102
46 45 71 3 Соль# 1661 4816
47 46 72 3 Ля 1760 4545
48 47 73 3 Ля# 1865 4290
49 48 74 3 Си 1976 4050

Табл. 2. Коэффициенты деления для всех нот.

В табл. 2 в ячейку E2 мы вставили формулу =ОКРУГЛТ(27,5*2^(B2/12); 1) и растянули её на всю колонку E. В ячейку F2 мы вставили формулу =ОКРУГЛТ(8000000/(27,5*2^(B2/12)); 1) и растянули её на всю колонку F.

На основании табл. 2 мы можем составить массив коэффициентов деления {61156, 57724, 54484, 51426, 48540, 45815, 43244, 40817, 38526, 36364, 34323, 32396, 30578, 28862, 27242, 25713, 24270, 22908, 21622, 20408, 19263, 18182, 17161, 16198, 15289, 14431, 13621, 12856, 12135, 11454, 10811, 10204, 9631, 9091, 8581, 8099, 7645, 7215, 6810, 6428, 6067, 5727, 5405, 5102, 4816, 4545, 4290, 4050}.

С таким же успехом, можно использовать 12 коэффициентов деления, но тогда прерывания будут выполнятся дольше, за счёт лишних операций по вычислению коэффициентов деления.

Замечание, так как мы планируем использовать в любом музыкальном произведении только 31 ноту (2,5 октавы), то у нас будет необходимость транспонировать, повышать или понижать тональность произведения, что мы легко сможем сделать сдвигая цифры в колонке A вверх или вниз.

Управление таймером-счётчиком TC1.

void setup() {
  // инициализация Timer/Counter-1
  pinMode (9, OUTPUT);                                                         // PB1/OC1A как Output порт
  TCCR1A = 0;
  TCCR1B = 0;
  TCCR1B &= ~((1 << CS10) | (1 << CS11) | (1 << CS12));                        // Таймер остановлен.
  TCCR1B |= (1 << WGM12);                                                      // CTC режим.
  TCCR1A &= ~((1 << COM1A1) | (1 << COM1A0) | (1 << COM1B1) | (1 << COM1B0));  // Порты PB1/PB2 от OC1A/OC1B отключены.
  TCCR1A = TCCR1A & ~(1 << COM1A0) | (1 << COM1A1);                            // Порт PB1 подключен к OC1A и обнуляется при совпадении.
  TCCR1A = TCCR1A & ~(1 << COM1A1) | (1 << COM1A0);                            // Порт PB1 подключен к OC1A и инвертируется при совпадении.
  OCR1A =   18182;                                                             // установка регистра совпадения. Нота Ля 440 Гц.
  TCCR1B |= (1 << CS10);                                                       // запуск с таймера с делителем на 1.
}

void loop() {

}

Лист. 2. Программное управление таймером счётчиком TC1.

Рис. 1. Осциллограмма снятая на 9 ножке платы Arduino Nano.

Принципиальная электрическая схема.

В программе лист. 2 мы организовали подключение выхода таймера-счётчика TC1 на порт микроконтроллера PB1, который, в свою очередь, подключён к pin 9 платы Arduino NANO или UNO.

Принципиальная электрическая схема музыкального автомата на Arduino NANO

Рис. 2. Принципиальная электрическая схема музыкального автомата.

На принципиальной электрической схеме рис. 2 однокаскадный импульсный усилитель на транзисторе 2N2222 своим входом подключён к pin 9 платы Arduino, а нагрузкой этого усилителя является высокоомный динамик. У транзистора 2N2222 типичный коэффициент усиления по току в схеме с общим эмиттером равен 300. В ключевом режиме работы ток коллектора транзистора примерно равен 5/30=0,16 Ампер. Где 5 Вольт напряжение питания схемы, а 30 Ом сопротивление динамика. Сопротивлением открытого транзистора можно пренебречь. Для полного открывания транзистора ток базы этого транзистора должен быть не менее 0,16/300=0,00055 Ампер. Где 0,16 Ампер ток коллектора транзистора, а 300 коэффициент усиления по току. Ограничивающий ток базы резистор должен быть не более 4,5/0,00055=8100 Ом. Где 4,5 Вольт напряжение единичного импульса на выходе микроконтроллера.

Например, Вы не нашли высокоомный динамик, типичное сопротивление маломощных динамиков 8 Ом, и такой у Вас есть. Задача: рассчитать сопротивление резистора в цепи базы транзистора. 5/8=0,625А. 0,625/300=0,002А. 4,5/0,02=2160 Ом.

Нотная грамотность.

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

основы музыкальной нотации

Рис. . Основы музыкальной нотации.

Лист. 3. Ноты Рождественской песенки Jingle Bells.

Закодируем эти ноты в соответствии с таблицами 1 и 2, для начала, каждой ноте будет соответствовать 2 цифры № ноты и её длительность. Коды сразу пишем в таблицу Google документы или LibreOffice Calc.

  A B C
1 № ноты Длительность Код
3 17 3 139
5 17 3 139
7 17 2 138
9 20 3 163
11 13 4 108
13 17 1 137
14 0 0 0

Табл. 3. Коды нот первых четырёх тактов песенки Jingle Bells.

В ячейку C3 табл. 3 мы поместили формулу =A2*8+B2 и растянули её на все следующие ячейки в колонке C.

В результате, у нас есть массив из кодов нот первых четырёх тактов песенки Jingle Bells и их длительностей:

{139, 139, 138, 139, 139, 138, 139, 163, 107, 108, 124, 137, 0}

Управление прерываниями Watchdog таймера (WDT).

Напишем программу инициализации прерываний WDT с примером использования этих прерываний.

// Массив из кодов нот первых четырёх тактов песенки Jingle Bells и их длительностей
byte volatile x[] = {1, 139, 139, 138, 139, 139, 138, 139, 163, 107, 108, 124, 137, 0};
// Массив коэффициентов деления для таймера-счётчика TC1
int volatile y[] = {0, 61156, 57724, 54484, 51426, 48540, 45815, 43244, 40817, 38526, 36364, 34323, 32396, 30578, 28862, 27242,
                    25713, 24270, 22908, 21622, 20408, 19263, 18182, 17161, 16198, 15289, 14431, 13621, 12856, 12135, 11454,
                    10811, 10204, 9631, 9091, 8581, 8099, 7645, 7215, 6810, 6428, 6067, 5727, 5405, 5102, 4816, 4545, 4290, 4050
                   };

ISR(WDT_vect) {                                                   // Обработчик прерываний WDT
  TCCR1A = TCCR1A & ~(1 << COM1A0) | (1 << COM1A1);               // Звук выкл.
  int static n = 0;                                               // Смещение в массиве из кодов нот
  byte t = 0;                                                     // Код длительности ноты
  if (x[n] != 0) {                                                // Если код ноты + длительность = 0 это КОНЕЦ муз. фрагмента
    if ((x[n] / 8) != 0) {                                        // Если Если только код ноты = 0 это ПАУЗА в муз. произведении
      if (x[n] / 8 == x[n - 1] / 8) {                             // Если № ноты = № предыдущей ноты, надо организовать СТАККАТО
        for (int i = 0; i < 500; i++) {                           // Задержка между соседними нотами
          digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
        }
      }
      digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
      t = x[n];                                                   // Вычисляем код длительности ноты
      t = ~t & 0b00000111;                                        // Код длительности ноты - это инвертированные младшие три бита кода ноты
      asm("wdr");                                                 // Сбрасываем WDT
      WDTCSR |= (1 << WDCE) | (1 << WDE);                         // Разрешить изменение значения предделителя WDT
      WDTCSR = t | (1 << WDIE );                                  // Устанавливаем длительность и разрешаем прерывания от WDT
      OCR1A = y[x[n] / 8];                                        // Выбор ноты
      TCCR1A = TCCR1A & ~(1 << COM1A1) | (1 << COM1A0);           // Звук вкл.
    }
    n++;                                                          // Приращение смещения в массиве, только если это не КОНЕЦ муз. фрагмента
  }
}

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode (9, OUTPUT);
  // инициализация WDT
  cli();                                               // Запрт прерываний
  asm("wdr");                                          // Сбрасываем WDT
  WDTCSR |= (1 << WDCE) | (1 << WDE);                  // Разрешить изменение значения предделителя WDT
  WDTCSR = (1 << WDP2) | (1 << WDIE );                 // Время 1/4 и разрешение прерывания от WDT
  sei();                                               // Разрешение прерываний
  // инициализация таймера-счётчика TC1
  TCCR1A = 0;
  TCCR1B = 0;
  TCCR1B &= ~((1 << CS10) | (1 << CS11) | (1 << CS12));                        // Таймер остановлен.
  TCCR1B |= (1 << WGM12);                                                      // CTC режим.
  TCCR1A &= ~((1 << COM1A1) | (1 << COM1A0) | (1 << COM1B1) | (1 << COM1B0));  // Порты PB1/PB2 от OC1A/OC1B отключены.
  TCCR1A = TCCR1A & ~(1 << COM1A0) | (1 << COM1A1);                            // Порт PB1 подключен к OC1A и обнуляется при совпадении.
  TCCR1B |= (1 << CS10);                                                       // запуск с таймера с делителем на 1.
}

void loop() {
}

Лист. 4. Программа исполняющая первые четыре такта песенки Jingle Bells.

В результате разработки программы лист. 4, был выработан формат массива кодов нот музыкального фрагмента и массива коэффициентов деления для таймера-счётчика TC1. Массив кодов нот музыкального фрагмента должен заканчиваться "0". Массив коэффициентов деления для таймера-счётчика TC1 должен начинаться с "0".

Размещение массива данных в Flash-памяти м-к.

Программа лист. 4 использует память м-к следующим образом:

Скетч использует 1360 байт (4%) памяти устройства. Всего доступно 30720 байт.
Глобальные переменные используют 123 байт (6%) динамической памяти, оставляя 1925 байт для локальных переменных. Максимум: 2048 байт.

Если хранить коды нот в динамической памяти, то мы сможем записать в м-к всего 1-2 мелодии. Flash память программ в м-к может хранить в десятки раз больше мелодий, но доступ к данным в Flash памяти специфичен. Перепишем программу лист. 4 так, что-бы массивы данных размещались в Flash памяти.

// Массив из кодов нот первых четырёх тактов песенки Jingle Bells и их длительностей
PROGMEM const byte notaCode[] = {139,139,138,139,139,138,139,163,107,108,124,137,147,
                                 147,147,148,148,147,139,139,140,140,139,123,123,139,
                                 122,162,139,139,138,139,139,138,139,163,107,108,124,
                                 137,147,147,147,148,148,147,139,139,140,140,163,163,
                                 147,123,105, 67,139,123,107, 66, 67, 68, 68, 67,139,
                                 123,107, 81, 83,147,139,123, 97,163,163,147,123,137,
                                  67,139,123,107, 81, 67,139,123,107, 82, 83, 83, 83,
                                 147,139,123,163,163,163,163,179,163,147,123,105,  0
                                };
// Массив коэффициентов деления для таймера-счётчика TC1
PROGMEM const word timerCounter[] = {0, 61156, 57724, 54484, 51426, 48540, 45815, 43244, 40817,
                                     38526, 36364, 34323, 32396, 30578, 28862, 27242, 25713, 24270,
                                     22908, 21622, 20408, 19263, 18182, 17161, 16198, 15289, 14431,
                                     13621, 12856, 12135, 11454, 10811, 10204, 9631, 9091, 8581, 8099,
                                     7645, 7215, 6810, 6428, 6067, 5727, 5405, 5102, 4816, 4545, 4290, 4050
                                    };

ISR(WDT_vect) {
  TCCR1A = TCCR1A & ~(1 << COM1A0) | (1 << COM1A1);    // Звук выкл.
  int static nNote = 0;                                // Номер ноты в музыкальном фрагменте
  boolean static staccato = 0;
  staccato = !staccato;                                // Если 0, задержка между соседними нотами - СТАККАТО
  if (staccato) {
    byte lengthNote = pgm_read_byte_near(notaCode + nNote);  // Код ноты и её длительности
    if (lengthNote) {                                    // Если код ноты + длительность = 0 это КОНЕЦ муз. фрагмента
      byte tmp = lengthNote;                             // Длительность ноты
      tmp = ~tmp & 0b00000111;                           // Код длительности ноты - это инвертированные младшие три бита кода ноты
      asm("wdr");                                        // Сбрасываем WDT
      WDTCSR = (1 << WDCE) | (1 << WDE);                 // Разрешить изменение значения предделителя WDT
      WDTCSR = tmp;                                      // Устанавливаем длительность
      WDTCSR = 0 | (1 << WDIE );                         // Запрещаем RESET и разрешаем прерывания от WDT
      nNote++;                                           // Приращение номера ноты в музыкальном фрагменте
      lengthNote = lengthNote / 8;                       // Вычисляем номер ноты в массиве timerCounter
      if (lengthNote) {                                  // Если номер ноты не равен 0
        OCR1A = pgm_read_word_near(timerCounter + lengthNote);        // Выбор ноты
        TCCR1A = TCCR1A & ~(1 << COM1A1) | (1 << COM1A0);             // Звук вкл.
      }
    }
  }
  else {                                               // Задержка между соседними нотами - СТАККАТО
    asm("wdr");                                        // Сбрасываем WDT
    WDTCSR = (1 << WDCE) | (1 << WDE);                 // Разрешить изменение значения предделителя WDT
    WDTCSR = 0 | (1 << WDIE );                         // Время 16 мсек. и запрещаем RESET и разрешаем прерывания от WDT
  }
}

void setup() {
  pinMode (9, OUTPUT);
  // инициализация WDT
  cli();                                               // Запрт прерываний
  asm("wdr");                                          // Сбрасываем WDT
  WDTCSR |= (1 << WDCE) | (1 << WDE);                  // Разрешить изменение значения предделителя WDT
  WDTCSR = 0 | (1 << WDIE );                           // Время 16 мсек. и запрещаем RESET и разрешаем прерывания от WDT
  sei();                                               // Разрешение прерываний
  // инициализация таймера-счётчика TC1
  TCCR1A = 0;
  TCCR1B = 0;
  TCCR1B &= ~((1 << CS10) | (1 << CS11) | (1 << CS12));                        // Таймер остановлен.
  TCCR1B |= (1 << WGM12);                                                      // CTC режим.
  TCCR1A &= ~((1 << COM1A1) | (1 << COM1A0) | (1 << COM1B1) | (1 << COM1B0));  // Порты PB1/PB2 от OC1A/OC1B отключены.
  TCCR1A = TCCR1A & ~(1 << COM1A0) | (1 << COM1A1);                            // Порт PB1 подключен к OC1A и обнуляется при совпадении.
  TCCR1B |= (1 << CS10);                                                       // запуск с таймера с делителем на 1.
}

void loop() {

}

Лист. 5. Программа исполняющая песенку Jingle Bells с использованием Flash памяти для хранения констант.

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

Скетч использует 1026 байт (3%) памяти устройства. Всего доступно 30720 байт.
Глобальные переменные используют 12 байт (0%) динамической памяти, оставляя 2036 байт для локальных переменных. Максимум: 2048 байт.

Управление воспроизведением музыкальных фрагментов.

Массив кодов нот песенки Jingle Bells, не содержит повторяющихся частей. Но, практически, любая песня содержит куплет и припев и, как правило, ноты всех куплетов не отличаются друг от друга, а у припева, и подавно.

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

Лист. 6. Ноты фортепианной пьесы Людвига ван Бетховена «К Элизе» (нем. Für Elise).

Обратите внимание на лист. 6, мы нашли в произведении Людвига ван Бетховена «К Элизе» один фрагмент мелодии повторяющийся 3 раза (окрашено в розовый цвет) и один такт повторяющийся 2 раза. Но это ещё не все повторения в этом произведении. Первая половина произведения «К Элизе» повторяется 2 раза. Причём, сначала звучит 9 тактов (в девятом такте нота ля длительностью 1/4). Обратите внимание, девятый и первый такт не полные. После звучания ноты ля длительностью 1/4 из девятого такта, начинается воспроизведение с первого такта в котором 2 ноты длительностью 1/16 каждая. В результате девятый такт вместе с первым имеют длительность 3/8.

Потом звучит 2, 3, 4, 5, 6, 7, 8 такты, а далее 10, 11 .... и до конца произведения.

Программист все повторения может просто запрограммировать. Но, для начала нам надо закодировать все музыкальные фрагменты произведения Людвига ван Бетховена «К Элизе» без повторений.

  A B C D
1 № ноты Длительность Код Адрес фрагмента
2 0 2 2 0
3 29 4 236 1
4 28 4 228 2
5 0 0 0 3
6 29 4 236 4
7 28 4 228 5
8 29 4 236 6
9 24 4 196 7
10 27 4 220 8
11 25 4 204 9
12 22 3 179 10
13 0 4 4 11
14 13 4 108 12
15 17 4 140 13
16 22 4 180 14
17 0 0 0 15
18 24 3 195 16
19 0 4 4 17
20 17 4 140 18
21 21 4 172 19
22 24 4 196 20
23 25 3 203 21
24 17 4 140 22
25 29 4 236 23
26 28 4 228 24
27 0 0 0 25
28 24 3 195 26
29 0 4 4 27
30 17 4 140 28
31 25 4 204 29
32 24 4 196 30
33 0 0 0 31
34 22 2 178 32
35 0 0 0 33
36 22 3 179 34
37 0 4 4 35
38 24 4 196 36
39 25 4 204 37
40 27 4 220 38
41 29 3 235 39
42 0 4 4 40
43 20 4 164 41
44 30 4 244 42
45 29 4 236 43
46 27 3 219 44
47 0 4 4 45
48 18 4 148 46
49 29 4 236 47
50 27 4 220 48
51 25 3 203 49
52 0 4 4 50
53 17 4 140 51
54 27 4 220 52
55 25 4 204 53
56 24 3 195 54
57 0 4 4 55
58 17 4 140 56
59 29 4 236 57
60 28 4 228 58
61 0 0 0 59
62 22 2 178 60
63 0 0 0 61

Табл. 4. Коды нот произведения Людвига ван Бетховена «К Элизе» без повторений.

В программе на языке C++ мы должны предусмотреть возможность воспроизводить некоторые музыкальные фрагменты по несколько раз.

// Массив инструкций переходов
PROGMEM const word order[] {4, 16, 4, 26, 32, 1, 4, 16, 4, 26, 34, 4, 26, 60, 0};
// Массив из кодов нот и их длительностей
PROGMEM const byte notaCode[] = {2, 236, 228, 0, 236, 228, 236, 196, 220, 204, 179, 4, 108, 140, 180, 0,
                                 195, 4, 140, 172, 196, 203, 140, 236, 228, 0, 195, 4, 140, 204, 196, 0,
                                 178, 0, 179, 4, 196, 204, 220, 235, 4, 164, 244, 236, 219, 4, 148, 236,
                                 220, 203, 4, 140, 220, 204, 195, 4, 140, 236, 228, 0, 178, 0
                                };
// Массив коэффициентов деления для таймера-счётчика TC1
PROGMEM const word timerCounter[] = {0, 61156, 57724, 54484, 51426, 48540, 45815, 43244, 40817,
                                     38526, 36364, 34323, 32396, 30578, 28862, 27242, 25713, 24270,
                                     22908, 21622, 20408, 19263, 18182, 17161, 16198, 15289, 14431,
                                     13621, 12856, 12135, 11454, 10811, 10204, 9631, 9091, 8581, 8099,
                                     7645, 7215, 6810, 6428, 6067, 5727, 5405, 5102, 4816, 4545, 4290, 4050
                                    };

ISR(WDT_vect) {
  TCCR1A = TCCR1A & ~(1 << COM1A0) | (1 << COM1A1);    // Звук выкл.
  int static nOrder = 0;                               // Номер инструкции в массиве Order
  int static nNote = 0;                                // Номер ноты в музыкальном фрагменте
  boolean static staccato = 0;
  staccato = !staccato;                                // Если 0, задержка между соседними нотами - СТАККАТО
  if (staccato) {
    byte lengthNote = pgm_read_byte_near(notaCode + nNote);  // Код ноты и её длительности
    if (lengthNote) {                                    // Если код ноты + длительность = 0 это КОНЕЦ муз. фрагмента
      byte tmp = lengthNote;                             // Длительность ноты
      tmp = ~tmp & 0b00000111;                           // Код длительности ноты - это инвертированные младшие три бита кода ноты
      asm("wdr");                                        // Сбрасываем WDT
      WDTCSR = (1 << WDCE) | (1 << WDE);                 // Разрешить изменение значения предделителя WDT
      WDTCSR = tmp;                                      // Устанавливаем длительность
      WDTCSR = 0 | (1 << WDIE );                         // Запрещаем RESET и разрешаем прерывания от WDT
      nNote++;                                           // Приращение номера ноты в музыкальном фрагменте
      lengthNote = lengthNote / 8;                       // Вычисляем номер ноты в массиве timerCounter
      if (lengthNote) {                                  // Если номер ноты не равен 0
        OCR1A = pgm_read_word_near(timerCounter + lengthNote);        // Выбор ноты
        TCCR1A = TCCR1A & ~(1 << COM1A1) | (1 << COM1A0);             // Звук вкл.
      }
    }
    else {                                             // КОНЕЦ муз. фрагмента
      if (pgm_read_word_near(order + nOrder)) {        // Если не конец произведения
        nNote = pgm_read_word_near(order + nOrder);
        nOrder ++;
      }
    }
  }
  else {                                               // Задержка между соседними нотами - СТАККАТО
    asm("wdr");                                        // Сбрасываем WDT
    WDTCSR = (1 << WDCE) | (1 << WDE);                 // Разрешить изменение значения предделителя WDT
    WDTCSR = 0 | (1 << WDIE );                         // Время 16 мсек. и запрещаем RESET и разрешаем прерывания от WDT
  }
}

void setup() {
  pinMode (9, OUTPUT);
  // инициализация WDT
  cli();                                               // Запрт прерываний
  asm("wdr");                                          // Сбрасываем WDT
  WDTCSR |= (1 << WDCE) | (1 << WDE);                  // Разрешить изменение значения предделителя WDT
  WDTCSR = 0 | (1 << WDIE );                           // Время 16 мсек. и запрещаем RESET и разрешаем прерывания от WDT
  sei();                                               // Разрешение прерываний
  // инициализация таймера-счётчика TC1
  TCCR1A = 0;
  TCCR1B = 0;
  TCCR1B &= ~((1 << CS10) | (1 << CS11) | (1 << CS12));                        // Таймер остановлен.
  TCCR1B |= (1 << WGM12);                                                      // CTC режим.
  TCCR1A &= ~((1 << COM1A1) | (1 << COM1A0) | (1 << COM1B1) | (1 << COM1B0));  // Порты PB1/PB2 от OC1A/OC1B отключены.
  TCCR1A = TCCR1A & ~(1 << COM1A0) | (1 << COM1A1);                            // Порт PB1 подключен к OC1A и обнуляется при совпадении.
  TCCR1B |= (1 << CS10);                                                       // запуск с таймера с делителем на 1.
}

void loop() {

}

Лист. 7. Программа исполняющая фортепианную пьесу Людвига ван Бетховена «К Элизе».

Массивы в музыкальной программе.

Массив order в программе лист. 7 содержит адреса первых нот музыкальных фрагментов в массиве notaCode. Фактически, в массиве order записаны индексы массива notaCode. Каждый элемент массива order - это инструкция, в каком порядке следует исполнять музыкальные фрагменты. Но, в нашей музыкальной программе так же нужны инструкции, позволяющие использовать в музыкальном произведении более 31 ноты. То есть, нужна инструкция, позволяющая тридцать одному номеру ноты поставить в соответствие различные наборы нот.

В программе лист. 7 в строке lengthNote = lengthNote / 8 мы из онобайтного кода ноты и её длительности вычисляем код ноты. Если к полученному коду ноты прибавить число (смещение), то мы получим новый набор соответствий кодов нот (номеров нот) и их частоте (высоте ноты). Чтобы можно было гибко выбирать музыкальный диапазон из 31 ноты, нам нужны смещения от 1 до 49 - 31 = 18 и мы охватим 3 октавы (можно и больше). Для 18 смещений создавать новый массив команд, или изменять структуру массива order, считаю, не рационально. Можно в массиве order считать число от 1 до 18 командой изменить высоту нот в музыкальном фрагменте, а числа от 20 до 32767 считать адресом музыкального фрагмента. Такой подход наложит не существенные ограничения на использование младших 20 адресов в массиве notaCode.

А что если использовать в массиве order значения меньше 32000 как адреса музыкального фрагмента, а числа от 32001 до 32018 как команду изменить высоту нот. Это рационально так как старшие адреса Flash памяти микроконтроллера не будут использоваться для хранения массива notaCode. Значит, так и поступим.

polonez

Лист. 8. Ноты Полонеза Огинского.

// Массив инструкций переходов
PROGMEM const word order[] {32016, 1, 1, 0};
// Массив из кодов нот и их длительностей
PROGMEM const byte notaCode[] = {0, 105, 100, 108, 115, 107, 75, 76, 68, 50, 75, 107,
                                 107, 146, 107, 131, 123, 114, 91, 163, 164, 148, 139,
                                 140, 116, 107, 108, 92, 107, 108, 92, 74, 75, 51, 68,
                                 76, 68, 52, 68, 92, 116, 108, 92, 76, 68, 76, 52, 76,
                                 108, 148, 170, 147, 164, 172, 164, 148, 164, 188, 212,
                                 204, 188, 172, 164, 148, 140, 148, 164, 140, 162, 146, 0
                                };
// Массив коэффициентов деления для таймера-счётчика TC1
PROGMEM const word timerCounter[] = {0, 61156, 57724, 54484, 51426, 48540, 45815, 43244, 40817,
                                     38526, 36364, 34323, 32396, 30578, 28862, 27242, 25713, 24270,
                                     22908, 21622, 20408, 19263, 18182, 17161, 16198, 15289, 14431,
                                     13621, 12856, 12135, 11454, 10811, 10204, 9631, 9091, 8581, 8099,
                                     7645, 7215, 6810, 6428, 6067, 5727, 5405, 5102, 4816, 4545, 4290, 4050
                                    };

ISR(WDT_vect) {
  TCCR1A = TCCR1A & ~(1 << COM1A0) | (1 << COM1A1);    // Звук выкл.
  byte static hTone = 0;                               // Высота тона, тональность
  int static nOrder = 0;                               // Номер инструкции в массиве Order
  int static nNote = 0;                                // Номер ноты в музыкальном фрагменте
  boolean static staccato = 0;
  staccato = !staccato;                                // Если 0, задержка между соседними нотами - СТАККАТО
  if (staccato) {
    byte lengthNote = pgm_read_byte_near(notaCode + nNote);  // Код ноты и её длительности
    if (lengthNote) {                                    // Если код ноты + длительность = 0 это КОНЕЦ муз. фрагмента
      byte tmp = lengthNote;                             // Длительность ноты
      tmp = ~tmp & 0b00000111;                           // Код длительности ноты - это инвертированные младшие три бита кода ноты
      asm("wdr");                                        // Сбрасываем WDT
      WDTCSR = (1 << WDCE) | (1 << WDE);                 // Разрешить изменение значения предделителя WDT
      WDTCSR = tmp;                                      // Устанавливаем длительность
      WDTCSR = 0 | (1 << WDIE );                         // Запрещаем RESET и разрешаем прерывания от WDT
      nNote++;                                           // Приращение номера ноты в музыкальном фрагменте
      lengthNote = lengthNote / 8;                       // Вычисляем номер ноты в массиве timerCounter
      if (lengthNote) {                                  // Если номер ноты не равен 0
        OCR1A = pgm_read_word_near(timerCounter + lengthNote + hTone);        // Выбор ноты
        TCCR1A = TCCR1A & ~(1 << COM1A1) | (1 << COM1A0);                     // Звук вкл.
      }
    }
    else {                                                         // КОНЕЦ муз. фрагмента
      if (pgm_read_word_near(order + nOrder)) {                   // Если не конец произведения
        // hTone = 0;                                            // Восстановить тональность
        if (pgm_read_word_near(order + nOrder) >= 32000) {      // Если order[n] команда изменить высоту нот
          hTone = pgm_read_word_near(order + nOrder) - 32000;  // Изменить тональность
          nOrder ++;
        }
        nNote = pgm_read_word_near(order + nOrder);
        nOrder ++;
      }
    }
  }
  else {                                               // Задержка между соседними нотами - СТАККАТО
    asm("wdr");                                        // Сбрасываем WDT
    WDTCSR = (1 << WDCE) | (1 << WDE);                 // Разрешить изменение значения предделителя WDT
    WDTCSR = 0 | (1 << WDIE );                         // Время 16 мсек. и запрещаем RESET и разрешаем прерывания от WDT
  }
}

void setup() {
  pinMode (9, OUTPUT);
  // инициализация WDT
  cli();                                               // Запрт прерываний
  asm("wdr");                                          // Сбрасываем WDT
  WDTCSR |= (1 << WDCE) | (1 << WDE);                  // Разрешить изменение значения предделителя WDT
  WDTCSR = 0 | (1 << WDIE );                           // Время 16 мсек. и запрещаем RESET и разрешаем прерывания от WDT
  sei();                                               // Разрешение прерываний
  // инициализация таймера-счётчика TC1
  TCCR1A = 0;
  TCCR1B = 0;
  TCCR1B &= ~((1 << CS10) | (1 << CS11) | (1 << CS12));                        // Таймер остановлен.
  TCCR1B |= (1 << WGM12);                                                      // CTC режим.
  TCCR1A &= ~((1 << COM1A1) | (1 << COM1A0) | (1 << COM1B1) | (1 << COM1B0));  // Порты PB1/PB2 от OC1A/OC1B отключены.
  TCCR1A = TCCR1A & ~(1 << COM1A0) | (1 << COM1A1);                            // Порт PB1 подключен к OC1A и обнуляется при совпадении.
  TCCR1B |= (1 << CS10);                                                       // запуск с таймера с делителем на 1.
}

void loop() {

}

Лист. 9. Программа исполняющая первую часть Полонеза Огинского.