Программирование звука в Linux
Андрей Боровский
borovsky@yandex.ru
Эта статья посвящена программированию звуковой подсистемы Linux. Мы рассмотрим различные аспекты программирования звука: от работы с основными устройствами до форматов хранения аудиоданных. Разумеется, в журнале нельзя полностью охватить материал, оригинальная документация по которому насчитывает сотни страниц, так что моя задача √ помочь читателю сориентироваться в многообразии устройств и средств программирования.
Оборудование и данные
Микшер
Большинство звуковых карт оборудовано микшерами. Микшер √ это устройство, позволяющее устанавливать уровни записи/воспроизведения для других устройств, а также выбирать источник записываемых данных.
Цифровое аудио
Цифровое аудио (digital audio) √ самый простой и естественный формат хранения и передачи звука в цифровых устройствах. Принцип цифрового аудио следующий: аналого-цифровой преобразователь считывает значения амплитуды аналогового сигнала через определенные интервалы времени. Каждое значение кодируется числом, которое записывается в одном или нескольких байтах (один такой код называется сэмплом). В стереозаписи сэмплы, соответствующие левому и правому каналам, либо записываются на одном треке, чередуясь друг за другом, либо размещаются непрерывно на отдельных треках. Рассмотренная ниже система OSS использует чередование сэмплов. При воспроизведении выполняется обратное преобразование из цифровой в аналоговую форму. Качество записи определяют частота дискретизации и разрядность кодирования амплитуды.
Максимальная частота дискретизации, поддерживаемая большинством звуковых карт, составляет 48 КГц. Карты класса Hi-End поддерживают частоты дискретизации до 96 КГц (DVD audio). Сэмплы кодируются 8, 16 и 24 битами (последнее значение поддерживается не всеми картами). Для записи одной секунды одноканального звука с частотой дискретизации 8 КГц и разрядностью сэмплов 8 бит (качество цифрового телефона) потребуется 8000 байтов.
Пропускная способность канала для передачи такого сигнала должна составлять 64 КБит/сек. Для записи одной секунды стереозвука с частотой дискретизации 44,1 КГц и разрядностью сэмплов 16 бит (что соответствует качеству аудио-CD) понадобится 172 КБ а пропускная способность канала должна быть не меньше 1.5 MБит/сек. Из этих расчетов становится очевидным, почему в большинстве форматов хранения цифрового аудио применяются различные методы сжатия данных.
MIDI
Протокол MIDI (Musical Instrument Digital Interface) используется электронными музыкальными инструментами, а также специальными устройствами воспроизведения звука. В MIDI передается не закодированный звуковой сигнал, а набор инструкций, описывающих мелодию, которую должны исполнять инструменты, поэтому запись в формате MIDI гораздо компактнее, чем запись в формате цифрового аудио. Главным недостатком MIDI с точки зрения программирования звука на компьютере является то, что этот протокол может воспроизводить только музыку, причем ее звучание будет зависеть от возможностей конкретной звуковой карты. Практически все звуковые карты оснащены портами для подключения внешних MIDI-устройств. Кроме того, большинство карт обладает встроенными MIDI-синтезаторами, позволяющими воспроизводить MIDI-музыку через аудиосистему самой карты.
Драйверы и интерфейсы
На сегодняшний день в Linux наиболее распространены две аудиоподсистемы: OSS и ALSA.
OSS (Open Sound System), разрабатываемая компанией 4Front Technologies, по замыслу разработчиков должна стать средством построения единого звукового интерфейса для различных UNIX-платформ и совместимых с ними систем. OSS представляет собой набор драйверов звуковых карт и библиотек, реализующих простые и удобные интерфейсы программирования. Система OSS распространяется в двух вариантах: OSS/Free и OSS Commercial. С сайта www.opensound.com можно бесплатно загрузить пробную версию OSS Commercial, обладающую более широкими возможностями по сравнению с OSS/Free и функционирующую в течение длительного периода времени. К достоинствам OSS следует отнести широкий спектр поддерживаемых звуковых карт и полноту проработки драйверов √ OSS позволяет работать со всеми типами встроенных в карты устройств, хотя новые карты не всегда поддерживаются полностью. Еще одно достоинство √ подробная документация.
Система ALSA (Advanced Linux Sound Architecture), как видно из названия, предназначена специально для Linux. Проект ALSA является открытым и начат сравнительно недавно (на момент написания этой статьи стабильная версия драйверов добралась лишь до номера 0.5.12). На сайте проекта www.alsa-project.org можно загрузить последнюю версию системы и документацию, которая еще далеко не полна. ALSA хорошо справляется с программированием микшера и цифрового аудио, однако поддержка MIDI-синтезаторов пока что отсутствует. Кроме того, при работе с некоторыми дистрибутивами Linux, ALSA не поддерживает устройство /dev/sndstat, позволяющее получать информацию о параметрах установленного звукового оборудования.
Главное достоинство ALSA √ полная открытость проекта, остальные преимущества носят пока скорее потенциальный характер.
В распоряжении Linux-программиста есть и другие пакеты, например, ориентированная на KDE система aRts. В примерах программ, приведенных в этой статье, используется OSS.
Программирование микшера
Драйверы OSS позволяют обращаться к микшерам посредством файлов /dev/mixer00, /dev/mixer01 и т.п. Обычно к первому из доступных микшеров можно также обратиться по символической ссылке /dev/mixer. Все основные константы OSS для работы с микшером (как, впрочем, и с другими аудиоустройствами) объявлены в файле soundcard.h. Интерфейс микшера позволяет считывать и устанавливать значения уровней записи/воспроизведения для различных каналов, а также указывать канал-источник записи.
Перед началом работы с микшером надо открыть соответствующий файл устройства (например, /dev/mixer). Дальнейшая работа с файлом выполняется при помощи функции ioctl. Чтобы определить, какие каналы поддерживает данный микшер, следует вызвать функцию ioctl, передав в параметре request константу SOUND_MIXER_READ_DEVMASK.
ioctl(mixer_fd, SOUND_MIXER_READ_DEVMASK, &mask);
Здесь mixer_fd √ дескриптор файла микшера. В переменной mask будет возвращена маска каналов. Каждый бит маски соответствует определенному каналу. Если бит установлен, канал поддерживается. Битам маски соответствуют константы, определенные в файле soundcard.h. Их имена начинаются с префикса SOUND_MASK_, за которым следует имя канала. Обозначения основных каналов приводятся в таблице:
Обозначение
|
Назначение
канала
|
VOLUME
|
Общий
уровень воспроизведения для
основного выхода звуковой карты
|
TREBLE
|
Уровень
воспроизведения высокочастотной
составляющей
|
BASS
|
Уровень
воспроизведения низкочастотной
составляющей
|
SYNTH
|
Встроенный
синтезатор
|
PCM
|
Устройства
/dev/dsp и /dev/audio
|
SPEAKER
|
Встроенный
динамик ПК. Канал работает только в
том случае, если встроенный динамик
напрямую подключен к звуковой карте.
|
LINE
|
Линейный
вход звуковой карты
|
MIC
|
Микрофон
|
CD
|
Аудио-CD
(устройство CD-ROM)
|
RECLEV
|
Общий
уровень записи
|
В возвращенной маске будут установлены только биты, соответствующие стереоустройствам. Для того чтобы определить, какие каналы поддерживают запись, ioctl вызывается точно так же, но с ключом SOUND_MIXER_READ_RECMASK, а для того чтобы проверить, какие каналы настроены на запись в данный момент √ с ключом SOUND_MIXER_READ_RECSRC.
Для установки канала в качестве источника записи служит вызов
ioctl(mixer_fd, SOUND_MIXER_WRITE_RECSRC, &mask);
Перед этим вызовом в переменную mask нужно записать бит канала, который мы хотим перевести в режим записи. Например, для микрофона это будет выглядеть так:
mask = SOUND_MASK_MIC;
Значения уровней записи/воспроизведения каналов представляются числами от 0 до 100. Для стереоустройств уровень можно установить отдельно для каждого канала. В этом случае уровень задается двухбайтовым значением (младший байт √ для правого канала, старший √ для левого).
Для операций с уровнями каналов микшера в файле soundcard.h определены константы. Имена этих констант начинаются с префиксов SOUND_MIXER_READ_ и SOUND_MIXER_WRITE_, определяющих соответственно операции чтения и записи. За префиксом следует указание канала, для которого выполняется операция. Является ли устанавливаемый уровень уровнем записи или воспроизведения, зависит от характера канала. Для входных каналов (MIC, CD, RECLEV) устанавливается уровень записи. Для выходных каналов (VOLUME, PCM, SYNTH) √ уровень воспроизведения. Таким образом, при установке, например, уровня записи для аудио-CD, вызов ioctl должен выглядеть следующим образом:
ioctl(mixer_fd, SOUND_MIXER_WRITE_CD, &vol);
В переменной vol передается устанавливаемое значение уровня. После вызова ioctl в переменной vol будет храниться значение уровня, фактически установленное микшером. Это значение может отличаться от того, которое вы пытались установить, так что его неплохо бы проверить. Текущее значение уровня записи с CD можно получить при помощи вызова
ioctl(mixer_fd, SOUND_MIXER_READ_CD, &vol);
В качестве примера приведу фрагмент программы, управляющей микшером. Эта программа открывает микшер, проверяет, поддерживается ли запись с микрофона, устанавливает микрофон в качестве источника записи, если он не установлен, считывает текущее значение уровня записи для микрофона и, если оно меньше 90, увеличивает его на 10.
#include <stdlib.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/soundcard.h>
#include <errno.h>
#define MIXER "/dev/mixer"
...
int mixer_fd, recmask, recsrc, vol;
if ((mixer_fd = open(MIXER, O_RDWR)) == -1) {
perror(MIXER);
return EXIT_FAILURE;
}
if (ioctl(mixer_fd, SOUND_MIXER_READ_RECMASK, &recmask) == -1) {
printf("Микшер неактивен, попробуйте другое устройство");
return EXIT_FAILURE;
}
if ((recmask & SOUND_MASK_MIC) == 0 ) {
printf("Запись с микрофона не поддерживается\n");
exit();
}
ioctl(mixer_fd, SOUND_MIXER_READ_RECSRC, &recsrc);
if ((recsrc & SOUND_MASK_MIC) == 0 ) {
recsrc = SOUND_MASK_MIC;
ioctl(mixer_fd, SOUND_MIXER_WRITE_RECSRC, &recsrc);
}
ioctl(mixer_fd, SOUND_MIXER_READ_MIC, &vol);
if (vol < 90) vol+=10;
ioctl(mixer_fd, SOUND_MIXER_WRITE_MIC, &vol);
close(mixer_fd);
...
Программирование канала цифрового аудио
Канал цифрового аудио позволяет как воспроизводить аудиоданные, так и считывать данные, поступающие от внешних устройств. В OSS устройствам воспроизведения и записи цифрового аудио соответствуют файлы /dev/dspXX, где XX √ номер устройства. Нумерация устройств начинается с 0, и обычно на файл /dev/dsp00 существует символическая ссылка /dev/dsp, соответствующая первому доступному устройству. При воспроизведении звука аудиоданные записываются в файл устройства, а при записи √ считываются из него. Перед началом записи необходимо с помощью программы, управляющей микшером, выбрать источник и установить значение уровня записи. Необходимо подчеркнуть, что в Linux нет специальных устройств, соответствующих источникам записи. При любом источнике (микрофон, аудио-CD и т. п.) запись производится с выходного устройства звуковой карты.
Для воспроизведения аудиоданных надо:
╥ открыть устройство для воспроизведения;
╥ установить параметры устройства (частоту дискретизации, разрядность, число каналов);
╥ записать данные в устройство;
╥ закрыть устройство.
Порядок действий при записи таков:
╥ открыть устройство для чтения данных;
╥ установить параметры устройства;
╥ прочитать данные из устройства;
╥ закрыть устройство.
При работе с OSS файл устройства dsp открывается при помощи функции open, а чтение и запись в файл выполняются функциями read и write. Параметры устройства устанавливаются при помощи функции ioctl. Во втором параметре ioctl передаются константы, определяющие операции с устройством. В таблице приведены наиболее важные операции:
Второй
параметр ioctl
|
Описание
|
Третий
параметр ioctl
|
SNDCTL_DSP_CHANNELS
|
установка числа каналов
|
число каналов (1 √ моно, 2 √ стерео)
|
SNDCTL_DSP_SETFMT
|
установка формата
|
формат кодирования
|
SNDCTL_DSP_SPEED
|
установка частоты дискретизации
|
частота дискретизации, Гц
|
SNDCTL_DSP_GETFMTS
|
получение данных о поддерживаемых
форматах
|
возвращает маску форматов
|
SNDCTL_DSP_SYNC
|
синхронизация передачи управления
программе с окончанием
воспроизведения звука
|
|
SNDCTL_DSP_RESET
|
быстрая остановка и перезапуск
устройства
|
|
В файле soundcard.h можно найти и другие константы. Вызовы SNDCTL_DSP_SETFMT и SNDCTL_DSP_GETFMTS оперируют идентификаторами форматов сэмплов. Наиболее часто используемые форматы:
╥ AFMT_U8 √ беззнаковый 8-битный;
╥ AFMT_S16LE √ знаковый 16-битный в формате ╚младший байт √ первый╩;
╥ AFMT_S32LE √ знаковый 32-битный (используются 24 бита) в формате ╚младший байт √ первый╩.
Значения третьего параметра ioctl всегда передаются при помощи указателя на переменную. После вызова ioctl эта переменная содержит фактическое значение параметра, которое может отличаться от затребованного.
Ниже приводится текст программы micrec.c, осуществляющей запись с микрофона (проверка ошибок опущена):
#include <stdlib.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/soundcard.h>
#define BUF_SIZE 2048
int audio_fd, out_fd;
void onexit() {
close(audio_fd);
close(out_fd);
}
int main (int argc, char *argv[]) {
int format, nchans, rate;
int actlen, count;
unsigned char buf[BUF_SIZE];
if (argc < 2) {
printf("команда: %s filename [rate]\n", argv[0]);
return EXIT_SUCCESS;
}
out_fd = open(argv[1], O_CREAT|O_WRONLY, 0777);
audio_fd = open("/dev/dsp", O_RDONLY, 0);
atexit(onexit);
format = AFMT_U8;
ioctl(audio_fd, SNDCTL_DSP_SETFMT, &format);
if (format != AFMT_U8) {
printf("ошибка: запрошенный формат не поддерживается");
return EXIT_FAILURE;
}
nchans = 1;
ioctl(audio_fd, SNDCTL_DSP_CHANNELS, &nchans);
if (argc == 3) rate = atoi(argv[2]);
else rate = 8000;
ioctl(audio_fd, SNDCTL_DSP_SPEED, &rate);
printf("Используемая частота дискретизации: %i Гц\n", rate);
for (count = 0; count <= (rate*30*nchans); count += actlen) {
actlen = read(audio_fd, buf, BUF_SIZE;
write(out_fd, buf, actlen);
}
return EXIT_SUCCESS;
}
Перед запуском программы нужно с помощью программы-микшера выбрать микрофон в качестве источника записи и установить значения уровней записи. Если у вас нет микрофона, можно записать данные с аудио-CD. В этом случае компакт-диск надо запустить до запуска программы. При записи с CD можно установить значение nchans равным 2, что соответствует стереозаписи. Учтите, что если вы попытаетесь записывать данные с не установленного источника (например, с входа Line In, к которому ничего не подключено), устройство dev/dsp может оказаться заблокированным, и вам, возможно, придется перезагрузить систему. У программы micrec есть обязательный параметр (argv[1]), в котором передается имя файла, в котором сохраняется запись и необязательный параметр (argv[2]) √ частота дискретизации (значение по умолчанию √ 8000). Программа выполняет запись в течение 30 секунд.
Далее следует текст программы micplay.c, предназначенной для воспроизведения файлов, созданных программой micrec.
#include <stdlib.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/soundcard.h>
#define BUF_SIZE 4096
int main (int argc, char *argv[]) {
int audio_fd, in_fd, format;
int nchans, rate, actlen;
unsigned char buf[BUF_SIZE];
if (argc < 2) {
printf("команда: %s filename [rate]\n", argv[0]);
return EXIT_SUCCESS;
}
in_fd = open(argv[1], O_RDONLY);
audio_fd = open(DEVICE, O_WRONLY, 0);
format = AFMT_U8;
ioctl(audio_fd, SNDCTL_DSP_SETFMT, &format);
if (format != AFMT_U8) {
printf("ошибка: запрошенный формат не поддерживается");
return EXIT_FAILURE;
}
nchans = 1;
ioctl(audio_fd, SNDCTL_DSP_CHANNELS, &nchans);
if (argc == 3) rate = atoi(argv[2]);
else rate = 8000;
ioctl(audio_fd, SNDCTL_DSP_SPEED, &rate);
printf("Используемая частота дискретизации: %i Гц\n", rate);
while ((actlen = read(in_fd, buf, BUF_SIZE)) > 0)
write(audio_fd, buf, actlen);
close(audio_fd);
close(in_fd);
return EXIT_SUCCESS;
}
Параметры у micplay такие же как у micrec. Чтобы услышать эффект ускорения или замедления, попробуйте воспроизвести файл, указав частоту дискретизации, отличающуюся от той, с которой он был записан.
Сохранение аудиоданных в wav-файле
Программа micrec записывает данные в ╚сыром╩ формате. Фактически, результат выполнения программы micrec √ это просто оцифрованный звук. Файл не содержит информации ни о частоте дискретизации, ни о формате кодирования, ни о числе каналов. Чтобы файлы, можно было воспроизводить стандартными проигрывателями, данные надо сохранять в одном из стандартных форматов. Самый простой из них √ Microsoft RIFF (wav-файлы). От нашего ╚сырого╩ формата wav-файлы отличаются только наличием заголовка, в котором указываются основные параметры аудиозаписи. Структура заголовка wav-файла такая:
typedef struct WAVHEADER_t {
// идентификатор формата файла
char riff[4];
// общий размер файла
long filesize;
// тип данных riff
char rifftype[4];
// идентификатор блока описания формата
char chunk_id1[4];
// размер блока описания формата
long chunksize1;
// идентификатор формата данных
short wFormatTag;
// число каналов
short nChannels;
// число сэмплов в секунду
long nSamplesPerSec;
// среднее число байт/сек
long nAvgBytesPerSec;
// размер одного блока (число каналов)*(число байтов на канал)
short nBlockAlign;
// число битов на один сэмпл
short wBitsPerSample;
// идентификатор области аудиоданных
char chunk_id2[4];
// длина области аудиоданных
long chunksize2;
} WAVHEADER_t;
Непосредственно за заголовком следуют данные. Определим процедуру fillheader, заполняющую заголовок wav-файла в соответствии с переданными ей параметрами:
void fillheader(WAVHEADER_t *header, short channels, long samplerate, short databits, long rawsize)
{
memcpy(header->riff , (const void *) "RIFF", 4);
memcpy(header->rifftype, (const void *) "WAVE", 4);
memcpy(header->chunk_id1, (const void *) "fmt ", 4);
header->chunksize1 = 16;
header->wFormatTag = WAVE_FORMAT_PCM;
memcpy(header->chunk_id2, (const void *) "data", 4);
header->nChannels = channels;
header->nSamplesPerSec = samplerate;
header->nBlockAlign = databits * channels / 8;
header->nAvgBytesPerSec = samplerate * header->nBlockAlign;
header->wBitsPerSample = databits;
header->chunksize2 = rawsize;
header->filesize = header->chunksize2 + 44;
};
Функции fillheader передаются следующие параметры: header √ указатель на структуру WAVHEADER_t, которую следует заполнить; channels √ число каналов (1 √ моно, 2 √ стерео); samplerate √ частота дискретизации в Гц; databits √ число битов, кодирующих сэмпл (8, 16 или 32); rawsize √ длина блока аудиоданных в байтах.
Разместив описание структуры WAVHEAER_t и объявление функции fillheader в файле wav.h, а текст функции fillheader в файле wav.c, мы можем написать следующую программу, преобразующую данные из формата программы micrec в формат wav:
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <sys/stat.h>
#include "wav.h"
#define BUF_SIZE 8192
int main(int argc, char * argv[]) {
struct WAVHEADER_t header;
struct stat fs;
int in_fd, out_fd, actlen, rate, nchans, fmt, aopt;
char buf[BUF_SIZE];
if (argc < 3) {
printf("команда: %s rawfile wavfile [-rxx] [-fxx] [-cxx]\n", argv[0]);
return EXIT_FAILURE;
}
rate = 8000;
nchans = 1;
fmt = 8;
if (stat(argv[1], &fs) == -1) {
perror(argv[1]);
return EXIT_FAILURE;
}
if ((in_fd = open(argv[1], O_RDONLY)) == -1) {
perror(argv[1]);
return EXIT_FAILURE;
}
if ((out_fd = open(argv[2], O_WRONLY|O_CREAT, 0777)) == -1) {
perror(argv[2]);
return EXIT_FAILURE;
}
while ((aopt = getopt (argc, argv, "r:f:c")) != -1)
switch (aopt) {
case 'r':
if (optarg != NULL) rate = atoi(optarg);
break;
case 'f':
if (optarg != NULL) fmt = atoi(optarg);
break;
case 'c':
if (optarg != NULL) nchans = atoi(optarg);
break;
}
fillheader(&header, nchans, rate, fmt, fs.st_size);
write(out_fd, &header, sizeof(header));
while ((actlen = read(in_fd, buf, BUF_SIZE)) > 0)
write(out_fd, buf, actlen);
close(out_fd);
close(in_fd);
}
Назовем нашу программу raw2wav. Программа запускается так:
./raw2wav rawfile wavfile [ -r rate ] [ -f format ] [ -c channels ]
где rawfile √ имя входного файла, wavfile √ имя результирующего файла в формате wav, rate √ частота дискретизации, format √ число бит на сэмпл, а channels √ количество каналов. Значения по умолчанию √ такие же, как у micrec.
Небольшое замечание: проигрыватель noatun 1.0.1 поставляемый с Mandrake 8.0 и используемый по умолчанию в KDE, не всегда корректно воспроизводит wav-файлы, полученные с помощью нашей программы. Видимо, проблема связана с noatun, так как другие проигрыватели, в том числе xmms и Windows Media Player, воспроизводят файлы без накладок.
На этом я завершаю рассказ о работе с цифровым аудио. В следующей статье речь пойдет о протоколе MIDI.
В начало
|