Картина Владимира Матвецева 'Сон'

 

'... и оркестр не заиграл, и даже не грянул, и даже не хватил, а именно, по омерзительному выражению кота, урезал какой-то невероятный, ни на что не похожий по развязности своей марш.'

М.А. Булгаков 'Мастер и Маргарита'

Спросите у хорошего портного, что является главным элементом мужской одежды, и наверняка, вы услышите - пуговица. Так и в случае дверного звонка, о разработке которого я продолжаю разговор, его звучание является одной из главных характеристик. В то же время, после ознакомления в интернете с описаниями звонков, воспроизводящих до 30 приятных мелодий, у меня возникла ассоциация с фразой, которую и поставил эпиграфом к этой статье. Не проблема, хотя и кощунство, “научить” дверной звонок спеть что-нибудь гламурное волшебным голосом Сары Брайтман, но в 3 часа ночи эта мелодия скорее всего заставит нас поглубже зарыться в одеяло в надежде увидеть продолжение столь же волшебного сна, какой видит персонаж одноименной картины Владимира Матвейцева. А ведь именно в этот момент соседи, живущие этажом ниже, до неприличия настойчиво жмут кнопку вашего мелодичного звонка, желая поболтать на тему “Для чего предназначены водопроводные краны в ванной комнате?”. Наверняка у вас есть свои истории на этот случай, но думаю мы сойдемся в мнении, что добрый марш, исполненный на хриплой боевой трубе, незаменим в данном случае.

Вывод: в данном приложении вполне приемлем звук низкого качества.

Коротко об истории музыкальных синтезаторов

Как вы увидите в следующей статье, где мы займемся схемотехникой “железа” и его программированием, воспроизведение звука стандартными средствами микрокомпьютера (MCU), занятие не сложное. Но т.к. этот цикл статей предназначен для описания полного процесса производства, то поговорим о главном, а именно о генерации мелодий. Ведь не с неба же они свалились прямо в электронные мозги нашего дверного звонка. А так как в своем интернет-магазине мы хотим предоставить покупателю возможность выбирать мелодии, то желательно научиться самостоятельно создавать их описания, понятные для MCU. Не станем же мы бессовестно вырезать куски из концертов Pink Floyd и запихивать их в программируемые звуковые чипы. Думаю трудно найти человека, который не слышал концерт этой группы “Темная сторона Луны” или “Животные”, где ярко представлен электронный синтезатор. Перед тем как углубиться в работу вспомним отца музыкальных синтезаторов, человека с удивительной биографией, артиллерийского полковника Евгения Мурзина, незаслуженно забытого в настоящее время. В далеком 1941году, еще будучи студентом он разработал первые чертежи фотоэлектронного оптического синтезатора звука, идея производства которого по-понятным причинам не получила поддержки в те тяжелые военные годы. Только в 1958 г. был изготовлен первый образец, который он назвал АНС в честь композитора А.Н. Скрябина. Для сравнения и осознания насколько проста наша задача отмечу, что синтезатор Мурзина имел 72 звука в октаве. Мы же будем работать со стандартными семью нотами в октаве.

Задача и инструменты

Т.к. мы уже вот-вот начнем программировать, то настало время выбора языка и средств разработки. Даже самый беглый взгляд на эту тему откроет вам великое множество языков и инструментов, предназначенных для написания и перевода текстов программ в массивы битов, понятные выбранному MCU. Среди них “бабушки и дедушки” типа Ada и Fortran, уже матерые Java, Питон, С и С++, а также молодой Rust. Конечно дело вкуса, но портами MCU можно управлять даже из PHP, например, с помощью PiPHP в случае Raspberry PI.

Вероятно многие не согласятся, но осмелюсь утверждать, что профессия программиста в теперешнем ее виде кардинально изменится в ближайшие 10-15 лет. Например, еще совсем недавно, создание Графического Интерфейса Пользователя (GUI) было серьезным трудоемким испытанием для программиста, а сейчас некоторые инструменты позволяют легко описать его простыми текстовыми конструкциями на XML, QML, либо графическими средствами, которые предоставляют многочисленные IDE. Но дело даже не в этом. Просто стандартный цикл смены человеческих поколений, исчисляемый 25-ю годами вряд-ли может значительно измениться, так как находится в прямой зависимости от физиологии человека. С другой стороны, цикл смены технологий уже в настоящий момент короче цикла смены поколений, и вероятно скоро достигнет 5 лет. В связи с этим, чтобы оставаться востребованным, программист столкнется с необходимостью постоянного изучения новых языков и средств работы с ними. При этом, он обязан успевать качественно исполнять текущую работу. Задайте себе вопрос о длительности наиболее продуктивного периода работы средне-статистического программиста и не будет ли он укорачиваться, хотя бы по причине постоянного увеличивающегося стресса? На мой взгляд мы уже близки к насыщению и программирование перейдет на более высокий системный уровень, продолжительность активности которого будет еще короче. В статье 'Маслобойня' вы также найдете интересный диалог касательно 'необходимости' иметь столь великое множество языков программирования.

Поэтому, я предлагаю простое правило – выберите относительно долговременное направление работы для исполнения которой необходимо знание трех-пяти языков, а свободное время посвятите более важному делу, как например, изучению “Книги перемен”. В большинстве случаев конечному пользователю не важно выбрал ты Erlang для решения задачи, или BASIC. Важно чтобы изделие имело реальную потребительскую ценность и надежно работало годами без ремонта и вмешательства разработчика. Хотя и это, казалось бы логичное требование, уже претерпело изменения, подчиняясь другим законам, не связанным напрямую с трудом инженеров. Во-всяком случае холодильники и стиральные машины раньше проектировались для более долговременной работы.

Итак, требуется создать программу, позволяющую проигрывать простые одноголосные мелодии на спикере Персонального Компьютера (PC). Текстовое представление музыкальных фрагментов должно легко читаться человеком, не знакомым с нотной грамотой, и не требовать специализированных музыкальных редакторов для написания. Программа должна иметь наглядные средства для оперативной модификации рисунка мелодии и возможность его сохранения в исходном файле описателя. Основным назначением программы является трансформация текстового представления окончательно отредактированной мелодии в стандартный файл настроек (в случае языка С++ это заголовочный файл с расширеним “.hh”), который будет подключаться на этапе компиляции программы для выбранного типа MCU, с учетом частоты его тактирования. При этом, такие параметры каждой ноты, как октава, высота звука, длительность звучания и длительность паузы, должны вместе помещаться в один байт (8 бит).

Из описания задачи следует, что программа относится к типу инструментов для творчества человека, не является звеном в цепи средств автоматической обработки заказа на производство изделия, и ввиду относительно большого количества параметров должна иметь GUI. Т.к. наш дверной звонок будет построен на очень дешевом маломощном MCU, то спикер PC в виде устройства воспроизведения звука для данной программы выбран потому, что аппаратные средства PC и методы генерации звука там очень похожи.

В качестве языка программирования выбран С++, а в качестве среды разработки Code::Blocks, т.к. этот бесплатный кросс-платформенное инструмент позволяет создавать широкий спектр приложений, включая внедренные (для различных типов MCU):

CodeBlocks GUI
Таким образом, овладение навыками работы с этой программой  позволит в дальнейшем во многих случаях отказаться от покупки или траты времени на изучение других инструментов подобного назначения. Операционная система как и прежде Linux, но не проблема использовать его, например, в Windows.

Создание исходного файла мелодии

Пытливый инженер конечно по-началу изучит хотя бы некоторые стандарты касательно синтеза музыки, например, MIDI и SoundFont. Затем откроет в Audacity, что-нибудь типа “Fly me to the Moon” в исполнении Оскара Питерсона и исследует мелодию на предмет неудачных, на его взгляд пассажей и формант. И уж потом удалит все “лишнее” и “сошьет” из нее нечто бодрящее, пользуясь только однотональными паттернами.

Melody Edit Audacity
По уже неоднократно упомянутой причине очень малых ресурсов MCU, мы максимально упростим задачу и не станем искать готовые решения или создавать OCR-сканер стандартного представления музыкальных произведений для нашей программы обработки первичного описателя мелодии:

Tea for two notes
По той же причине, мы не станем нагружать MCU обработкой сложных MIDI-структур, значит нет смысла усложнять и упомянутую программу, обрабатывающую текст музыкальной композиции. Тем более, что реальная мелодия, предназначенная для исполнения даже на простейшем музыкальном синтезаторе, обычно разбита на партии для различных инструментов. Нам нужен простой формат записи мелодий, достаточный для решения задачи соответственно поставленным условиям. Поэтому, если вы как и я не владеете нотной грамотой, то вооружитесь своим 100% слухом и наиграйте одним пальцем фрагмент требуемой мелодии, например, в приложении Virtual MIDI Piano Keyboard:

Virtual MIDI Piano Keyboard

В итоге получим, что-нибудь подобное упомянутому “Tea for two”, показанное в следующей паре строк, где каждая нота представлена в Американской Стандартной Системе Нотации (STN), которую еще называют Научной Нотацией:

B3,G3#,A3#,G3#,B3,G3#,A3#,A3#
A3#,F3#,G3#,F3#,A3#,A3#,G3#

Как видим, здесь есть указание на октаву (3) и высоту звука ноты (B, G,...), но нет данных о длительности нот и пауз. Длительность нот обычно не сильно варьируется в музыкальном произведении, тем более в случае мелодий для нашего приложения. А вот пауза... Это великое изобретение во взаимоотношениях людей! На следующей картинке показан фрагмент интермедии, где в течение нескольких минут статусы персонажей относительно друг друга изменяются в диапазоне от зависимого до лидирующего и наоборот. Прекрасные актеры настолько умело показывают эти метаморфозы, что не нужно вникать в смысл слов и даже смотреть на экран - достаточно просто слушать паузы, чтобы определить текущий статус говорящего.

Boss and Staff
И раз уж музыка призвана отображать жизнь, то мы расширим диапазон описания паузы хотя бы до четырех состояний. Впрочем, в некоторых случаях это правило можно было бы упростить - у годовалого ребенка пауза всегда одинаково доброжелательна.

Итак, создадим нотную грамматику для нашего простого приложения:

  • представление каждой ноты всегда состоит не менее, чем из двух символов в научной нотации, с возможным дополнением третьего символа альтернации '#'
  • поддерживается 2 типа длительности ноты – обычная и длинная. Длительная нота имеет тип шрифта 'жирный'
  • поддерживается 4 типа паузы – которые отличаются цветом шрифта: отсутствует, короткая, обычная, длительная.
  • описатели нот могут отделяться друг от друга символом ','

Таким образом, наша специализированная нотная грамматика позволяет представлять простые мелодии в более-менее удобном виде для чтения человеком. Теперь надо определиться с инструментом, посредством которого мы будем создавать эти первичные описатели. Можно воспользоваться широкими возможностями wxWidgets и wxSmith, позволяющими расширить разрабатываемую программу полем редактирования текста, поддерживающим управление шрифтами, соответственно требования нашей грамматики. С другой стороны, т.к. это будет не коммерческое и крайне редко используемое приложение, то вероятно вы согласитесь, что проще предоставить пользователю возможность использовать какой-то знакомый ему текстовый редактор.

Т.е. пора определиться с форматом файла первичного описателя мелодии, из которого наша программа должна сформировать заголовочный файл, "понятный" компилятору для выбранного MCU. Скопируйте вышеуказанный пример описателя мелодии в редактор 'Microsoft Word' или 'LibreOffice Writer' и сохраните его в разных форматах. Попробуйте, например, RTF, XML, HTML и помотрите их содержимое простейшим текстовым редактором. Вероятно вы будете немало удивлены объемом разнообразной служебной информации и, возможно, сочтете HTML самым подходящим для обработки парсером, который вам предстоит создать в разрабатываемой программе. Можно также попробовать использовать какой-нибудь Markdown Editor для создания первичного описателя мелодии. В общем, выберите подходящий формат и, конечно-же, ознакомьтесь с описывающим его стандартом.

Интересно, какую упрощенную грамматику вы бы предложили для поддержки 72-х звуков в октаве у синтезатора Мурзина?

Описание программы

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

B3,G3#,A3#,G3#,B3,G3#,A3#,A3#
A3#,F3#,G3#,F3#,A3#,A3#,G3#

На следующем рисунке показан GUI программы в среде разработки Code::Blocks:

Melody Header File Gen Software
Как видим он поддерживает настройку пауз и длительностей, объявленных в нашей грамматике. Дополнительно можно указать тип MCU и частоту его тактирования. Т.к. звучание мелодии на спикере отличается от того, которое мы слышали, наигрывая ее в приложении Virtual MIDI Piano Keyboard, то может пригодиться возможность сдвига оригинальной мелодии на нужное число октав. При этом, программа должна учитывать указанную частоту MCU и сообщать, при необходимости, о невозможности проигрывания выбранной мелодии с заданными конкретными параметрами. И наконец, можно указать количество воспроизводимых нот в случае, если звонок имеет батарейное питание (радио-кнопка). Мелодия все же должна быть узнаваема, поэтому нельзя грубо указать, например, число 5 для всех случаев.

Со временем, у каждого программиста вырабатывается свой стиль написания кода, но все же желательно ознакомиться с правилами работы известных людей и компаний, как например, NASA. Т.к. главной задачей разрабатываемого приложения является создание заголовочного файла описания мелодии, автоматически подставляемого при обработке заказа (вспомним описание нашего артикула) и компиляции программы для выбранного MCU, то добавлю к этим правилам еще одно:

  • старайтесь делать обработчики прерываний в программе MCU максимально короткими. В большинстве случаев в нем достаточно только лишь установить определенный флаг, который будет обработан в основном цикле программы, или в коде обработки выхода MCU из состояния сна. Если желательно все же производить определенные действия в обработчике прерывания, то они должны быть максимально просты.

В этой связи обратим еще раз обратим внимание на приведенную ранее формулировку конечной зачачи:

При этом, такие параметры каждой ноты, как октава, высота звука, длительность звучания и длительность паузы, должны вместе помещаться в один байт (8 бит).

Пора определиться с функцией, посредством которой мы будем управлять спикером.

Управление спикером PC - вариант №1

В приведенном ниже примере генерируется звук частотой 2 Кгц и длительностью 1 сек.

int freq = 2000; // Note frequency in Hz
int dur = 1000; // Note duration in ms

int fd = open("/dev/console", O_WRONLY);
ioctl(fd, KIOCSOUND, (int)(1193180/freq));
usleep(1000 * dur);
ioctl(fd, KIOCSOUND, 0);
close(fd);

Но, мы услышим его только в том случае, если запустим программу от имени sudo. Поэтому, данный вариант не рассматриваем в качестве подходящего для нашей цели.

Управление спикером PC - вариант №2

Используем функцию “beep”, полный аналог которой есть в Windows. Ее возможности в полной мере соответствуют нашей грамматике и позволяют установить частоту звучания ноты, ее длительность, а также длительность паузы. В среде Linux ее поддержка вполне возможно отключена по-умолчанию. Установить и заставить работать можно в следующей последовательности:

  • Установить 'beep':
    sudo apt-get install beep
  • В файле '/etc/modprobe.d/blacklist.conf' закомментировать строку:
    blacklist pcspkr
    и перезагрузить компьютер.
  • Проверить работу команды 'beep', например:
    beep -f 1000 -r 2 -d 1000 -l 400
  • Если звука нет, то в терминале ввести команду:
    alsamixer
    Откроется диалог, показанный на следующем рисуне, где надо разрешить канал 'Beep':

ALSAmixer
ПРИМЕЧАНИЕ:
если компьютер не имеет спикера, то для вывода на внешние динамики можно применить команду 'speaker-test'. Например:

speaker-test -t sine -b 1000 -p 1000 -f 1000

Но, т.к. характер звучания внешних динамиков кардинально отличается от звучания спикера, то данный метод я здесь не рассматриваю.

Функции элементов GUI программы

На представленном выше рисунке GUI показано состояние программы, после того как пользователь выбрал пункт “File” в меню и открыл файл с описателем требуемой мелодии. Если структура его содержимого соответствует правилам нашей грамматики, то будет разрешено активировать кнопку “Generate”. При нажатии на нее сформируется массив, содержащий частоты, соответствующих каждому символу из описателя мелодии, длительности нот и пауз, а также будут произведены некоторые дополнительные проверки. Данные о соответствии частоты символу ноты легко найти в интернете и, например, для 3-ей октавы их можно представить в виде следующей таблицы:

C3 = 130.813
C3# = 138.591
D3 = 146.832
D3# = 155.563
E3 = 164.814
F3 = 174.614
F3# = 184.997
G3 = 195.998
G3# = 207.652
A3 = 220.000
A3# = 233.082
B3 = 246.942

Если проблем нет, то программа запретит кнопку “Generate”, но разрешит кнопки “Play” и “Save”. При нажатии на кнопку “Save”, обновляется исходный файл описателя мелодии, соответственно изменениям параметров, которые пользователь внес посредством GUI, а также создается или обновляется заголовочный файл с расширением “.hh”, соответствующий выбранному MCU и частоте его тактирования. Кнопка “Generate” разрешается при любом оперативном изменении параметров, представленных в GUI программы. При этом проигрывание мелодии останавливается и сохранение запрещается.

Реализация описанных функций не представляет особого интереса, поэтому остановимся на кнопке “Play”. Xотя в GUI, построенных на основе wxWidgets можно использовать функцию wxExecute, позволяющую выполнять синхронные и асинхронные вызовы, мы рассмотрим другие варианты проигрывания мелодии.

Синхронное проигрывание мелодии

Схематичный обработчик нажатия кнопки “Play”:

for (i = 0; i < melody_notes_cntr; i++) {
   // Создаем команду для проигрывания каждой ноты
   cur_note_cmd = 'beep ';
   ......
   cur_note_cmd += '-D ' + cur_duration_value;

   // В итоге должны получить строку команды, например: 'beep -f 1000 -l 400 -D 1000'
   system(cur_note_cmd); // Исполняем ее
}

Этот рабочий вариант, но нам придется ждать проигрывания всей мелодии, т.к. GUI с нажатой кнопкой “Play” застынет в ожидании ее окончания. Нам же нужно иметь возможность остановить проигрывание в момент изменения любого параметра, и тут же попробовать проиграть модифицированный вариант.

Асинхронное проигрывание мелодии – добавляем Процессы

Создадим следующие переменные:

pid_t child_pid, first_child_pid;
volatile sig_atomic_t player_ready = 1;

Схематичный обработчик нажатия кнопки “Play”:

first_child_pid = fork(); // Создаем дочерний процесс для цикла сканирования нот

if (first_child_pid == 0) {
   player_ready = 1;

   for (i = 0; i < melody_notes_cntr; i++) {
      // Заполняем массив аргументов 'arg_list' для исполнения очередной ноты
      ......

      // Пример содержимого массива:
      // arg_list[1] = '-f 1000';
      // arg_list[2] = '-l 400;
      // arg_list[3] = '-D 1000';

      // Озвучиваем ноту
      if (player_ready == 1) {
         player_ready = 0;

         // Создаем дочерний процесс для исполнения команды 'beep'
         if ((child_pid = fork()) == 0) {
            execvp ("beep", arg_list);
            return;
          }

          // Ждем его завершения
          while (player_ready == 0) {}
      }
   }
}

Для того, чтобы отследить момент завершения звучания текущей ноты и оповестить об этом родительский процесс цикла сканирования мелодии, необходимо создать обработчик сигнала от дочернего процесса:

void player_signal_handler(int sig_num) {
   switch(sig_num) {
   case SIGCHLD:
      player_ready = 1;
      break;
   default:
      break;
   }
}

Чтобы прервать проигрывание мелодии, надо завершить соответствующие процессы, поэтому добавим, например, следующую функцию (показан фрагмент):

void stop_player(bool play_only = false) {
   if (child_pid > 0) {
      kill(child_pid, SIGTERM);
      child_pid = -1;
   }

   if (first_child_pid > 0) {
      kill(first_child_pid, SIGTERM);
      first_child_pid = -1;
   }
}

Ну и наконец, в обработчик каждого параметра GUI необходимо добавить код вызова функции 'stop_player'. Например, для списка выбора типа MCU код может быть таким:

void MelodyHeaderFileGenFrame::OnchoMCUSelect(wxCommandEvent& event){
   stop_player();
}

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

Асинхронное проигрывание мелодии – Нити

Нити могут быть созданы внутри текущего процесса, а также проще в управлении и синхронизации.
Создадим следующую переменную:

pthread_t thread_player_id;

Нам также понадобится функция сканирования мелодии внутри нити, которая по-сути повторяет функцию, которую мы описали, рассматривая синхронное проигрывание мелодии:

void* thread_player(void *args) {
   for (i = 0; i < melody_notes_cntr; i++) {
      // Создаем команду для проигрывания каждой ноты
      cur_note_cmd = 'beep ';
      ......
      cur_note_cmd += '-D ' + cur_duration_value;

      // В итоге строка команды, например: 'beep -f 1000 -l 400 -D 1000'
      system(cur_note_cmd); // Исполняем ее
   }
}

Функцию 'stop_player' отредактируем следующим образом:

void stop_player(bool play_only = false) {
   pthread_cancel(thread_player_id);
}

Схематичный обработчик нажатия кнопки “Play” также модифицируем:

pthread_create(&thread_player_id, NULL, thread_player, NULL);

И конечно добавим вызов функции 'stop_player' в обработчики параметров мелодии. Например:

void MelodyHeaderFileGenFrame::OnspnNotesOnBatteryChange(wxSpinEvent& event) {
   melody_len_on_battery = spnNotesOnBattery->GetValue();
   stop_player(true);
}

Заголовочный файл для MCU

В завершение несколько слов о файле с расширением '.hh', ради которого затевалась вся эта история и который, как упоминалось ранее, программа автоматически создаст и заполнит, соответственно исходному файлу мелодии и текущим параметрам, установленным в GUI. Вот его содержимое:

#define MELODY_BASE_PRESCALER 4
#define MELODY_LEN_ON_BATTERY 8
const unsigned char melody[15] PROGMEM = {123,8,74,8,27,72,10,106,106,6,72,6,10,10,104};

Я не сторонник неоправданных излишеств, но при желании даже в MCU, имеющем всего 1000 байт памяти программ, можно выделить 150-200 байт на десяток простых мелодий.

ПРИМЕЧАНИЕ: обратите внимание, что все определения в данном файле не имеют уникальных признаков, однозначно идентифицирующих мелодию. Здесь также нет определений длительностей нот и пауз, которые унифицированы для данного приложения и вынесены в отдельный заголовочный файл. Это сделано с целью экономии ресурсов MCU и накладывает ограничения на выбор типа мелодий, которые должны иметь одинаковый, как мы определили в начале статьи, бодрый ритм. Иначе указанные параметры должны определяться в каждом подобном заголовочном файле. Таким образом, если звонок должен поддерживать более одной мелодии, то автоматический сканер заказа обработает все требуемые музыкальные файлы и создаст единый, который будет подключен к остальным файлам проекта для выбранного MCU.

Инструменты и документы

Языки программирования: С++ (https://isocpp.org/get-started)
Инструменты: Code::Blocks (http://www.codeblocks.org/)
Virtual MIDI Piano Keyboard (https://sourceforge.net/projects/vmpk/)
Дополнительное чтение:

MIDI (https://www.midi.org/specifications)
SoundFont (http://freepats.zenvoid.org/sf2/sfspec24.pdf)

JavaScript (https://www.javascript.com/)
MIDI.js (https://mudcu.be/midi-js/)
Python (https://www.python.org/)
XML (https://www.w3.org/XML/)
JSON (http://www.json.org/)

Qt (https://www.qt.io/)
PyCharm (https://www.jetbrains.com/pycharm/)
Kivy (https://kivy.org/#home)
Git (https://git-scm.com/)
GitKraken (https://www.gitkraken.com/)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Продолжение следует...