Музыка на Arduino

Музыка на Arduino

Программа, исполняющая на Arduino музыку в формате PLAY Qbasic

Когда-то давным-давно, в 80х годах XX века, первые 8битные компьютеры получили возможность играть музыку. Программировалась музыка на интерпретаторе Бэйсик. Тогда она казалась очень оригинальной и необычной. Сейчас, когда компьютеры, телефоны, часы, игрушки и даже поздравительные открытки забиты MP3 музыкой и полифоническими MIDI, есть ли интерес покупать микроконтроллер, который значительно дороже, чем готовая китайская игрушка, и самому пытаться запрограммировать мелодию ? Тем не менее, программирование музыки при изучении работы микроконтроллера это такое же must be forever, как и незабвенная мигалка светодиодом.

Примеры, которые я нашел в Сети либо слишком примитивные, либо слишком сложные и нерабочие. В этой статье описывается программа, которая использует тот же формат нот, что и оператор PLAY QBasic. Таким образом, можно использовать старинные архивы BAS файлов или набирать и отлаживать музыку в Qbasic, а затем переносить готовые строки в программу (скетч) Arduino

Есть одна небольшая проблема. QBasic нормально работает под Windows-98 и Windows-XP, но для более поздних версий и для XP лучше запускать его с помощью эмулятора DOSBox

Для того, чтобы заставить 8-битный микроконтроллер Arduino играть музыку не требуется сложная дополнительная электроника. Достаточно взять пьезо-пищалку и подключить её одним контактом к выводу GND (Земля) платы контроллера, а другим - к свободному цифровому выводу (в данном примере - вывод 8). Если Вы хотите применить низкоомный динамик, надо подключить его через резистор 100 Ом (см рисунок), чтобы избежать токовой перегрузки процессора. Все подключения надо делать естественно при отключенном питании. Плата Arduino UNO может питаться как от внешнего источника питания, так и через USB, причем выбор источника происходит автоматически. Для питания схемы с пищалкой достаточно просто подключить плату кабелем к USB компьютера.




Оператор QBasic PLAY имеет такой вид:


PLAY "T120o3L8CDEFGGL16GFL8EFFL16FEDL8CDG4"

что означает

T120 - темп 120 тактов в минуту
o3 - октава 3
L8 - длительность нот по умолчанию - восьмые
Далее идут коды нот, причем после кода ноты могут идти символы + и - (диез и бемоль), длительнось или точка. Полное описание команд оператора PLAY следующее:


PLAY строка_команд$

строка_команд$  Выражение, состоящее из одной или нескольких
                следующих команд PLAY:
(ПРЕФИКСЫ)
Команды октавы и тона:
Oоктава Задает текущую октаву (0 - 6).
<    Переход на октаву выше.
>    Переход на октаву ниже.

Команды длительности и темпа:
Lразмер Задает длительность каждой ноты по умолчанию (1 - 64).
        L1 - целая нота,L2 - 1/2 ноты и т.д.
ML      Вид исполнения legato.
        (Ноты звучат в полную длительность)
MN      Вид исполнения normal.
        (Нота звучит 7/8 длительности, затем 1/8 - пауза)
MS      Вид исполнения staccato.
        (Нота звучит 3/4 длительности, затем 1/4 - пауза)
Tтемп   Задает темп исполнения в четвертях в минуту (32-255).
        По умолчанию T120.
(НОТА)
A - G   Играет определенную ноту текущей октавы.
        "CDEFGAB" - до,ре,ми,фа,соль,ля,си
Pпауза  Задает паузу (1 - 64).
        P1 - пауза в целую ноту,P2 - пауза в 1/2 ноты и т.д.
Nнота   НЕ ПОДДЕРЖИВАЕТСЯ моей программой.

(СУФФИКСЫ)
Команды изменения ноты:
# или + Диез. (Нота на полтона выше).
-       Бемоль. (Нота на полтона ниже).
.       Длительность 3/2 от размера ноты.
        (то же самое, что нота с точкой)

Также смотрите PLAY Statement

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


//PIN к которому подключена пищалка
#define SPEAKER 8
//PIN сведодиода на плате
#define LED 13
// То же, что и ==, чтобы не мучаться из-за опечаток
#define eq  ==
//символ a - цифра ?
#define isdigit(a) (((a)>='0') && ((a)<='9'))
//Символ a - строчная латинская буква ?
#define islower(a) (((a)>='a') && ((a)<='z'))
char octave=0;
char lendef=1;
char flagdiez=0;
int tempo=120;
char playmode='N'; 

Так как микроконтроллер у нас 8-битный, используются там, где это возможно, переменные типа char и unsigned char (для уменьшения бинарного кода программы), но за этим однобайтовым типом надо внимательно следить, чтобы избежать переполнений и неверных преобразований.

Мы подходим к музыке. Музыка хранится в массиве notes[]. Для ясности программы музыку лучше хранить в .h файлах. Вы просто берёте строки PLAY из программы .BAS Qbasic и копируете их в .h файл. Не забудьте, что музыка в этом примере должна быть только одна, все остальные #include нужно закомментировать.

//Выберите массив музыки notes[].
//Закомментируйте все прочие строки include.
//char notes[]="T120MSo3L8CDEFGGL16GFL8EFFL16FEDL8CDG4";
//#include "music.h"
//#include "jingle.h"
//#include "merry.h"
#include "silent.h"
//#include "bach1.h"
//#include "inventio.h"

Для вывода звука используются две небольшие функции playTone и playNote:


// собственно аппаратный вывод звука
// tone - полупериод импульса (микросекунды), а не частота звука
// duration - длительность звука (миллисекунды)
void playTone(int tone,int duration)
{digitalWrite(LED,HIGH)
for (unsigned long i=0;i < duration*1000L;i+=(tone*2))
  {digitalWrite(SPEAKER,HIGH);
   delayMicroseconds(tone);
   digitalWrite(SPEAKER,LOW);
   delayMicroseconds(tone);
  }
digitalWrite(LED,LOW)
}

//Преобразование кода ноты в тон
void playNote(char note,int duration)
{char names[]="CDEFGAB";
int tones[]={
//note tone
 1915 , 1700 , 1519 , 1432 , 1275 , 1136 , 1014 ,
//n+
 1806 , 1603 , 1433 , 1350 , 1202 , 1071 , 956 ,
//n-
 2029 , 1801 , 1610 , 1517 , 1351 , 1204 , 1074};
for (char i=0;i < 7;i++) 
    {if (names[i]==note)
     playTone(tones[i+flagdiez*7] >> octave,duration);}
}

Массив tones[] содержит длительности полупериодов импульсов в микросекундах (не частоты) нот, Первая строка массива соответствует обычным 7 нотам (до,ре,ми,фа,соль,ля,си), вторая строка - это те же тона, что и в первой, но деленные на 1.06 (корень 12й степени из 2), третья строка - те же величины, что и в первой, но умноженные на 1.06. Таким образом, вторая строка соответствует диезам, а третья - бемолям.

Наконец, мы подходим к интерпретатору музыкальных команд PLAY. Переменная i продвигает указатель на символы массива notes[i], при этом декодируются команды и ноты. Обратите внимание на подсвеченную строку char notecode=notes[i];i++; Она выполняется в цикле безусловно. Поэтому, если даже в логику обработки префиксов-суффиксов вкрадётся ошибка или что-то напутано в буквах notes[], программа не зависнет, а худшем случае просто не проиграет какие-то ноты, но цикл for безусловно завершится.


void setup()
{//декларация переменных
...
pinMode(LED,OUTPUT);
pinMode(SPEAKER,OUTPUT);
tempo=120;int note1=(int)(240000L/tempo);
//цикл чтения строки PLAY
for (int i=0;notes[i] != 0;)
{
retry:
//обработать команды-префиксы
...
//получить код ноты
char notecode=notes[i];i++;
//обработать команды-суффиксы
...
//играть ноту
if (islower(notecode)) notecode-=32;
if (notecode=='P') delay(notelen); else
    {if      (playmode eq 'L') {playNote(notecode,notelen);}
     else if (playmode eq 'S') {playNote(notecode,notelen*3/4);
                                delay(notelen/4);}
     else                      {playNote(notecode,notelen*7/8);
                                delay(notelen/8);}
    }
} // конец цикла for
} //конец функции setup

2 апреля 2020 Мелкие ошибки исправлены
Загрузить скетч Музыка на Arduino (77 KB)

Загрузить QBasic и коллекцию музыки (422 KB)

29 июня 2020 Добавлена музыка ZX Spectrum, Apple II, IBM PC XT
Загрузить музыку с компьютеров 80х годов (122 KB)

Продолжение. Более простая 1 байтная музыка Arduino