13 июля 2009 г.

Пишем пищалку для будильника

Часто мне не хватает удобного и простого будильника под Linux. Органайзеры со всплывающими окошками и проигрыванием заданной мелодии явно избыточны для того, чтобы просто не забыть про чайник. А старый добрый beep я использовать не могу по причине отсутствия PC-Speaker. Можно бы, конечно, поискать подходящую программу, но проще написать её самому. Тем более, программирование звука — это просто!

Итак, что должна уметь делать программа? Во-первых, издавать какой-то звук. Причем используя для этого звуковую карту. А во-вторых, должна быть консольной с возможностью указания времени звучания в командной строке.

Чтобы проигать какой-то звук я решил использовать библиотеку libao — очень простую в использовании и удобную. При этом ещё и кроссплатформенную. К слову, она поддерживает ALSA, ESD, OSS, Pulse, чего должно хватить с головой.

На родном сайте есть ссылки для скачивания и подробная документация. Пользователи Debian/Ubuntu могут установить библиотеку обычным способом

sudo aptitude install libao2
sudo aptitude install libao-dev

Пакет libao2 — это сама библиотека, он нужен будет для работы программы, а libao-dev — это заголовочные файлы.

Библиотека написана на чистом C, поэтому для удобства можно обернуть все необходимые функции в класс¸ а потом, если потребуется, выделить в отдельную библиотеку. Удобство может показаться сомнительным, но лично мне в основной программе не хочется вводить посторонние переменные и писать функции для открытия/закрытия устройства. Да и забыть могу. А так всё, что нужно, прописано в конструкторе и деструкторе, а значит вызывается само собой.

Работу с библиотекой можно разбить на три этапа:

  1. инициализация, настройка формата вывода, открытие устройства;

  2. воспроизведение звука;

  3. закрытие устройства и завершение работы с библиотекой.


Класс можно написать, например, такой:

class Beeper {
private:
ao_device *device;
int driver;
ao_sample_format format;

public:
Beeper();
void operator() (double freq, double time, double volume = 0.75);
~Beeper();
};

С конструктором и деструктором всё понятно, а вот перегруженная операция вызова функции — это и есть метод класса, который издает звуки. Ему передается всего три параметра: freq — частота звучания в герцах, time — время звучания в секундах, volume — громкость (от 0.0 до 1.0).

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

Можно взять синусоиду, тогда мы получим чистый тон. Частота колебаний определяет высоту звука, а амплитуда — громкость. Тут всё просто. На практике же мы сталкиваемся со следующей проблемой: компьютер-то работает не с математическими формулами, а с числами.

Но и тут проблема легко решается. Звуковая карта получает на вход последовательность чисел, задающих амплитуду через некоторые малые промежутки времени. Количество таких отсчетов амплитуды в секунду называется частотой дискретизации. Типичное значение — это 44100 Гц. Почему именно такое число? Дело в том, что человек может слышать звуки с частотой не выше ≈20000 Гц. А по теореме Котельникова, чтобы точно передать сигнал с какой-то частотой, нужна минимум в два раза большая частота дискретизации. Хотя, для низких звуков такая высокая частота — излишество.

Другие важные параметры — это разрядность и количество каналов (моно, стерео).

Разрядность говорит, сколько бит тратится на один отсчет амплитуды. Чем больше разрядность, тем точнее передается звук. Но для обычных нужд хватит и 16 бит, то бишь двух байт. Тут возникает такой вопрос: а как хранить эти два байта? Сначала старший или младший? Это старая компьютерная проблема. Я же решил не заморачиваться и сначала помещаю младший байт, а потом старший (так называемый little-endian порядок байтов). Для указания, что используется именно такой параметр, соответствующему полю структуры, отвечающей за формат сигнала присваивается значение AO_FMT_LITTLE.

Разбить двубайтовой целое на два однобайтовых легко. Для этого можно использовать побитовое И с числом 0xFF. Это даст младщий байт, так как старшие биты будут умножены на 0. А потом можно сдвинуть все старшие биты в исходном числе на 8 бит влево и повторить процедуру.

Для 16 битного звука значения амплитуды должны быть в диапазоне от -32768 до 32767. Эти крайние величины соответствуют максимальной громкости. Если нужен звук потише, то амплитуду можно уменьшить. Амплитуда, равная 0 — это тишина.

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

Итак, как издать звук. Для этого в памяти создается буфер, чтоб не отправлять каждый байт поодиночке. Буфер должен иметь размер t×F×b, где t — это время звучания, F — частота дискретизации, а b — разрядность в байтах. Потом мы в цикле записываем значения синусоиды с нужной частотой и амплитудой. А затем всё это дело выводится на устройство. Подробности можно посмотреть в коде программы.

Когда класс готов, можно заняться самой пищалкой. В ней просто несколько раз повторяется звук с частотой 880 Гц (это ля второй октавы) и длительностью 0,3 с. Пауза между звукам — 0,2 с. По умолчанию сигнал повторяется 20 раз (то есть 20/(0,3+0,2)=10 с), но можно передать нужную дительность как параметр командной строки.

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

После того, как программа скомпилирована, её можно поместить, например, в /usr/local/bin или создать deb-пакет, чтоб потом можно было поделиться с друзьями.

Использовать её просто. Например, нужно подать сигнал в 12:30, а исполняемый файл называется beep. Тогда пишем следующее:

echo "beep" | at 12:30

Если beep лежит в домашней папке, то не забываем указать путь:

echo "~/beep" | at 12:30

У at очень гибкий формат указания времени исполнения, так что рекомендую заглянуть в man.

Можно также всё это оформить в виде скрипта:

#!/bin/sh
echo "~/beep" | at $@

$@ — это специальная переменная, хранящая параметры, переданные скрипту. Так что можно будет просто написать, например:

alarm.sh 12:30


А вот вся программа в одном файле.

#include <ao/ao.h>
#include <cmath>
#include <cstdlib>

#define BITS 16
#define CHANNELS 1
#define RATE 22050

class Beeper {
private:
ao_device *device;
int driver;
ao_sample_format format;

public:
Beeper() {
ao_initialize();
this->driver = ao_default_driver_id();

this->format.bits = BITS;
this->format.channels = CHANNELS;
this->format.rate = RATE;
this->format.byte_format = AO_FMT_LITTLE;

this->device = ao_open_live(driver, &format, NULL);
if (this->device == NULL)
exit(1);
}

void operator() (double freq, double time, double volume = 0.75) {
int buf_size = static_cast<int>(this->format.bits/8 * this->format.channels * this->format.rate * time);
char *buffer = new char[buf_size];
int sample;

for (int i = 0; i < this->format.rate * time; i++) {
for (int c = 0; c < this->format.channels; c++) {
sample = static_cast<int>(volume * (1<<(this->format.bits - 1)) * sin(2 * M_PI * freq * ((float) i/format.rate)));
for (int j = 0; j < this->format.bits/8; j++) {
buffer[this->format.bits/8*i*this->format.channels + this->format.channels*c + j] = sample & 0xff;
sample >>= 8;
}
}
}
ao_play(device, buffer, buf_size);
delete[] buffer;
}

~Beeper() {
ao_close(device);
ao_shutdown();
}
};

int main(int argc, char **argv)
{
Beeper b;

int count = (argc == 1) ? 20 : atoi(argv[1]);

for (int i=0; i < count*2; i++){
b(880.0, 0.3);
b(0.0, 0.2);
}

return 0;
}

Компилируется командой

g++ -lao -o beep beep.cpp

Вот, вкратце всё. Не забываем покритиковать! ;)

3 комментария:

  1. Творческий подход! А я всегда sleep 10m && mplayer horn.mp3 пользуюсь :)

    ОтветитьУдалить
  2. У меня просто не было horn.mp3… :)

    ОтветитьУдалить
  3. merupakan slot online tanpa potongan dan terpercaya
    merupakan slot online tanpa potongan dan deccasino terpercaya yang menyediakan permainan yang tidak Bermain 인카지노 Judi Slot Online Terlengkap; 제왕 카지노 Slot Online Slot Online.

    ОтветитьУдалить