Предисловие
Привет, Мир!
Уже года 3 хочу написать что-нибудь на Хабр, но никак не находилось темы, на которую можно было бы накатать пост. Так было до тех пор пока мне не понадобилось узнать немного про работу системного таймера и системного динамика для лабораторной работы. Порыскав немного в интернете, я не нашел ничего дельного: что-то было написано слишком сложным языком, что-то было не особо содержательно. Я обзавелся неплохой книгой, целой ночью и попытался сыграть всеми известную тему из игры Марио. Продолжение прямо под катом, вроде бы у вас тут так заведено.
Дисклеймер
Код написан так как он написан. Автор не гений программирования, а всего лишь студент, но тем не менее, попытался написать максимально читаемый и понятный код. Всё было написано на Borland C и было протестировано в DOSBox только потому, что нет установленного доса и не очень хочется напортачить с часами реального времени.
В далеком 2012 году Loiqig уже написал более крутую версию, но, как мне показалось, мало внимания уделил теории.
Так же автор (т.е. я) имеет 4 года музыкального образования и был плох в сольфеджио (музыкальная грамота).
Немного теории
Давным давно, когда был популярен процессор Intel 8086, а IBM PC не вызывало вопросов, в эти самые IBM PC и совместимых с ним компьютерах использовался Intel 8253 — таймер и счетчик интервалов. В современных компьютерах этим занимается южный мост (Источник: Wikipedia).
Примерная логическая схема Intel 8253 PIT:
Как можно видеть на изображении выше, таймер подключен к линии IRQ0. Он вырабатывает прерывание 8h 18.2 раз в секунду.
Таймер состоит из 3 счетчиков (COUNTER0-2), которые работают независимо друг от друга.
Как бы это было не странно, но каждый счетчик выполняет свою работу. В современных компьютерах первый канал считает время суток. С помощью второго канала происходит регенерация DRAM. С помощью третьего канала можно сделать псевдослучайный генератор чисел и пиликать системным динамиком.
Каждый канал имеет 6 режимов работы:
- Режим 0 — прерывание терминального счета
- Режим 1 — программируемый ждущий мультивибратор
- Режим 2 — импульсный генератор частоты
- Режим 3 — генератор меандра
- Режим 4 — программно формируемый строб
- Режим 5 — аппаратно формируемый строб
Переходим к делу
Итак, мы узнали немного теории о системном таймере, узнали, что третий канал подключен к системному динамику. Вроде бы всё классно. Только как с помощью этого сыграть тему из Марио? Пока не понятно.
Каждый счетчик (канал) программируется отдельно. Мы уже решили, что нужно использовать третий. Как видно на изображении выше, динамик соединён с выходом OUT. В то же время он соединен с портом 61h, с помощью которого мы можем управлять динамиком. Первый (младший) бит подключен к входу Gate2 и определяет работает ли счетчик или нет. Второй бит запускает динамик.
Исходя из этой теории становится понятен фронт работы для проигрывания звука:
- Программируем CLOCK2 на нужную нам частоту (об этом позже)
- С помощью первых двух бит 61h включить динамик
Так как дефолтный системный динамик может воспроизводить только одноголосые звуки (ну это как было на старых кнопочных телефонах, где бумер наигрывали), то нам нужно получить частоту каждой ноты.
Как можно увидеть из таблицы, чтоб повысить/понизить октаву, нужно просто умножить/поделить значение частоты на 2.
Для того, чтоб установить нужную частоту для нашего счётчика нужно воспользоваться определенной формулой: 1193182 / N Гц, где 1193182 — частота таймера (1.193182 МГц если быть правильным), N — частота ноты, которую вы решили вывести в динамик.
// Функция проигрывания звука
// _freq — частота ноты
// _dur — длина ноты
// _del — задержка между двумя нотами
void play_sound(int _freq, int _dur, int _del)
{
outp(0x43, 0xb6); // Устанавливаем счетчик в режим 2 (меандра)
int timer_soundFreq = TIMER_FREQUENCY/_freq; // Получаем нужную нам
//частоту.
// TIMER_FREQUENCY = 1193182
// Загрузка частоты в регистр счетчика
outp(0x42, timer_delay & 0x00ff); // Сначала младший байт
outp(0x42, (timer_delay & 0xff00) >> 8); // Потом старший
outp(0x61, inp(0x61) | 3); // Включаем динамик
delay(_dur); // Делаем задержку, чтоб нота проигравалась именно столько,
// сколько нам нужно
outp(0x61, inp(0x61) & 0xfc); // Выключаем динамик
delay(_del); // Делаем задержку между двумя нотами
}
Функция main у меня до ужасного простая и, честно говоря, плохо оптимизированная, но сути дела это не меняет.
int main(int argc, char const *argv[])
{
for (size_t i = 0; i < N; ++i) // N — количесто нот, которые проигрываются
{
play_sound(FREQUENCY[i], DURATION[i], DELAY[i]);
}
return 0;
}
Так что же с нашим Марио?
Мы научились воспроизводить звуки через системный динами. Отлично! Но как же нам сыграть мелодию из Марио?
Воспользовавшись магией гугления мы находим ноты:
Далее придется вспомнить курс нотной грамоты и выписать каждую ноту:
ми2 — 1/4
ми2 — 1/8
до2 — 1/8
ми2 — 1/4
соль2 — 1/4
соль — 1/4
до2 — 1/4
соль — 1/4
ми — 1/4
ля — 1/4
си — 1/4
си (бемоль) — 1/8
ля — 1/4
соль — 1/4
ми2 — 1/4
соль2 — 1/4
ля2 — 1/4
фа2 — 1/8
соль2 --1/8
ми2 — 1/4
до2 — 1/8
ре2 — 1/8
си — 1/8
соль2 — 1/4
ми (диез)2 — 1/4
фа (бекар)2 — 1/8
ре (диез)2 — 1/4
ми (диез)2 — 1/8
соль (диез) — 1/4
ля (диез) — 1/4
до2 — 1/4
ля — 1/4
до2 — 1/4
ре2 — 1/4
соль2 — 1/4
фа (диез)2 — 1/4
фе (бекар)2 — 1/8
ре2 — 1/4
ми2 — 1/4
до3 — 1/4
до3 — 1/8
до3 — 1/4
Стоит отметить, что я ввел несколько допущений. В реальной мелодии играется по две ноты одновременно. Синхронизировать два досбокса мне не хотелось, поэтому я играл по одной ноте.
Вооружившись еще большим терпением переводим каждую ноту в частоты и собираем массивы частот и длительностей:
// Mario -- 43
int FREQUENCY[] = {659.255, 659.255, 659.255, 523.251, 659.255, 783.991, 391.995, 523.251, 391.995, 329.628, 440, 493.883, 466.164, 440, 391.995, 659.255, 783.991, 880, 698.456, 783.991, 659.255, 523.251, 587.33, 987.767, 783.991, 680.255, 698.456, 622.254, 680.255, 415.305, 466.164, 523.251, 440, 523.251, 587.33, 783.991, 739.989, 729.989, 587.33, 659.255, 1046.502, 1046.502, 1046.502};
int DURATION[] = {300, 300, 160, 160, 300, 300, 300, 300, 300, 300, 300, 300, 160, 300, 300, 300, 300, 300, 160, 160, 300, 160, 160, 160, 300, 300, 160, 300, 160, 300, 300, 300, 300, 300, 300, 300, 300, 160, 300, 300, 300, 160, 300};
int DELAY[] = {35, 35, 50, 35, 35, 350, 200, 35, 35, 200, 35, 35, 35, 200, 35, 35, 35, 35, 35, 200, 35, 35, 35, 35, 35, 35, 35, 35, 200, 35, 35, 35, 35, 35, 200, 35, 35, 35, 35, 200, 35, 35, 0};
В данной мелодии я насчитал 43 ноты. Тут тоже стоит заметить, что задержку между двумя соседними нотами я выбирал на слух. Получилось немного медленнее, чем в оригинале.
Заключение
В заключении хочу сказать, что работа с железом иногда получается более интересной, чем написание кучи высокоуровневого кода.
Если вдруг у кого-нибудь возникнет желание улучшить мою мелодию или написать что-то свое, то милости прошу в комментарии.
P.S.
Если же вы решили, что хотите поиграть на системном динамике и вы не знаете основ нотной грамоты, то под спойлером можно найти пару хинтов.