Программирование звука в Linux

Андрей Боровский,

kylixportal@narod.ru

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

Работа с аудио CD.

Изначально лазерные компакт-диски разрабатывались именно как носители оцифрованного звука, и "исторические последствия" этого ощущаются до сих пор (например, емкость дисков CD/R CD/RW традиционно измеряется в минутах). Практически любое устройство чтения CD-ROM является по совместительству и плеером аудио CD и позволяет воспроизводить аудиодиски при минимальном вмешательстве со стороны системы. Драйверы устройств чтения CD-ROM предоставляют как функции контроля воспроизведения аудиодисков средствами устройства (эти функции обычно используются программами-плеерами), так и функции непосредственного чтения аудиоданных (эти функции используются в основном программами-рипперами (rippers)).

В Linux интерфейс драйвера CD-ROM описан в файле linux/cdrom.h.

Воспроизведение аудио CD.

Запись на любом компакт-диске состоит из нескольких треков. Треки нумеруются начиная с нуля (трек 0 содержит оглавление диска). Номер трека не может превышать значение 99. На аудио CD каждый музыкальный фрагмент как правило записывается на отдельном треке. В определенных ситуациях одно произведение может быть записано на нескольких треках, или же наоборот, на одном треке может быть записано несколько независимых фрагментов. Последний вариант применяется, когда число фрагментов, которые необходимо записать на диск, превышает 99. В этом случае, для различения фрагментов внутри одного трека используются индексы. На одном и том же диске могут быть записаны как аудио-данные, так и другая информация. Перед воспроизведением трека с такого "смешанного" диска следует проверять, является ли трек аудио-треком или треком данных.

Параметры цифрового аудио, применяемые при записи аудио CD (CD-DA), приводятся в следующей таблице:

Разрядность сэмплов

16 бит

Частота дискретизации

44100 Гц

Число каналов

2 (стерео)

Формат записи каналов

чередование сэмплов

Запись на диске разбивается на фреймы. Каждый фрейм содержит 2352 байта. Нетрудно подсчитать, что для обеспечения указанных выше характеристик цифровой записи чтение данных должно выполняться со скоростью 75 фреймов в секунду (что и соответствует однократной скорости чтения CD-ROM). С фреймами связан и один из форматов адресации на аудио CD. Адресация осуществляется в единицах MSF - минуты, секунды, фреймы - где фрейм можно рассматривать как 1/75 секунды. Другой формат адресации, связанный с логическими блоками (LBA), используется в основном при работе с не-аудиодисками и для нас не так интересен.

В нижеследующей таблице приводятся основные вызовы ioctl, связанные с воспроизведением аудио CD.

Вызов

Описание

Дополнительный параметр

CDROM_DRIVE_STATUS

Получение данных о состоянии устройства

константа CDSL_XXX

CDROM_DISC_STATUS

Получение данных о диске

константа CDSL_XXX

CDROMREADTOCHDR

Чтение заголовока оглавления диска

структура cdrom_tochdr

CDROMREADTOCENTRY

Чтение элемента оглавления диска

структура cdrom_tocentry

CDROMSUBCHNL

Чтение данных о параметрах воспроизведения

структура cdrom_subchnl

CDROMPLAYTRKIND,

CDROMPLAYMSF

Воспроизведение аудиозаписи

Структуры cdrom_ti и cdrom_msf

CDROMSTOP

Остановка воспроизведения

значение 0

CDROMPAUSE, CDROMRESUME

Приостановка, возобновление воспроизведения

значение 0

CDROMEJECT

Открытие лотка устройства

значение 0

CDROMCLOSETRAY

Закрытие лотка устройства

значение 0

Вызовы CDROM_DRIVE_STATUS и CDROM_DISC_STATUS отличаются тем, что результат возвращается не в параметре-ссылке, а как значение функции ioctl. В качестве третьего аргумента ioctl выступает одна из констант CDSL_XXX, определенных в файле cdrom.h. Эти константы предназначены для работы с устройствами автоматической смены компакт-дисков (CD changers). В случае "однодискового" устройства следует использовать CDSL_CURRENT. Результатом вызова CDROM_DRIVE_STATUS могут быть значения CDS_NO_DISC (нет диска в устройстве), CDS_DRIVE_NOT_READY (устройство не готово), CDS_DISC_OK (диск обнаружен), а также некоторые другие константы из файла cdrom.h. Среди значений, возвращаемых вызовом CDROM_DISC_STATUS следует отметить CDS_NO_DISC (см. выше) CDS_AUDIO (диск опознан как аудио) и CDS_MIXED (диск опознан как "смешанный"). Остальные значения соответствуют не-аудиодискам.

Вызовы CDROMREADTOCHDR и CDROMREADTOCENTRY предназначены для работы с оглавлением диска. Вызов CDROMREADTOCHDR позволяет получить данные о номере первого и последнего информационных треков на диске, а вызов CDROMREADTOCENTRY - данные об отдельном треке - адрес начала трека (в формате MSF или LBA), тип трека (аудио или данные) и т.п. Вызов CDROMSUBCHNL позволяет получить информацию о текущем состоянии устройства - находится ли диск в режиме воспроизведения, и в какой позиции выполняется чтение данных.

Вызовы CDROMPLAYTRKIND и CDROMPLAYMSF запускают воспроизведение аудиозаписи. При этом первый вызов позволяет задать начало и конец воспроизводимого фрагмента значениями трек/индекс, а второй - адресами в формате MSF. Поскольку оглавление диска содержит данные о начальных адресах треков, воспроизведение отдельного трека часто выполняется по принципу "от начала данного трека до начала следующего". В файле cdrom.h определена константа CDROM_LEADOUT, указывающая на условный трек, расположенный после последнего трека.

Вызовы CDROMSTOP, CDROMPAUSE и CDROMRESUME выполняют, соответственно, остановку, временную остановку (пауза) и возобновление воспроизведения.

Рассмотрим пример приложения, собирающего данные о вставленном в устройство CD-ROM компакт-диске и воспроизводящего аудио-треки:

#include <stdio.h>
#include <linux/cdrom.h>
#include <sys/ioctl.h>
#include <sys/fcntl.h>
#include <errno.h>
int fd;
int main (int argc, char argv[]) {
int val;
struct cdrom_subchnl info;
struct cdrom_tochdr toc;
struct cdrom_ti index;
struct cdrom_tocentry entry;
int i, track;
if ((fd = open("/dev/cdrom", O_RDONLY|O_NONBLOCK)) == -1) {
perror("cdrom");
return 1;
}
if ( ioctl(fd, CDROM_DRIVE_STATUS,
CDSL_CURRENT) != CDS_DISC_OK ) {
printf("Устройство не готово\n");
return 1;
}
info.cdsc_format = CDROM_MSF;
ioctl(fd, CDROMSUBCHNL, &info);
if (info.cdsc_audiostatus == CDROM_AUDIO_PLAY) {
printf("Диск в режиме воспроизведения\n");
printf("трек: %i, время воспроизведения: %i%s%i\n",
info.cdsc_trk, info.cdsc_reladdr.msf.minute,
(info.cdsc_reladdr.msf.second<10) ? ":0" : ":",
info.cdsc_reladdr.msf.second);
return 0;
}
ioctl(fd, CDROMREADTOCHDR, &toc);
printf("Начальный трек: %i, конечный трек: %i\n",
toc.c
dth_trk0, toc.cdth_trk1);
entry.cdte_format = CDROM_MSF;
for (i = toc.cdth_trk0; i <= toc.cdth_trk1; i++) {
entry.cdte_track = i;
ioctl(fd, CDROMREADTOCENTRY, &entry);
if ((entry.cdte_ctrl & CDROM_DATA_TRACK) != 0)
printf("трек %i не содержит аудиоданных\n
", i);
else
printf("трек %i содержит аудиоданные; начало (MSF): %i:%i:%i\n",
i, entry.cdte_addr.msf.minute,
entry.cdte_addr.msf.second, entry.cdte_addr.msf.frame);
}
printf("Укажите трек для воспроизведения ");
scanf("%i", &track);
index.cdti_trk0 = track;

index.cdti_ind0 = 0;
index.cdti_trk1 = track;
index.cdti_ind1 = 255;
if (ioctl(fd, CDROMPLAYTRKIND, &index) < 0) {
printf("Невозможно воспроизвести трек %i\n", track);
return 1;
}
close(fd);
return 0;
}

Программа не контролирует процесс воспроизведения, а лишь посылает команды устройству CD-ROM. Воспроизведение трека продолжится и после завершения программы.

Другой пример - программа-риппер считывающая данные с аудио-треков и сохраняющая их в wav-файле. Подобную операцию можно было бы выполнить путем записи аудио-сигнала средствами звуковой карты, однако в этом случае вряд ли удастся избежать искажений. К тому же непосредственное чтение данных выполняется гораздо быстрее. Для чтения данных мы воспользуемся вызовом CDROMREADAUDIO, специально предназначенным для считывания информации CD-DA.

#include <stdio.h>
#include <unistd.h>
#include <sys/fcntl.h>
#include <linux/cdrom.h>
#define BUF_SIZE 75*2352
typedef struct wav_header {
char riff[4];
long filesize;
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;
} wav_header;
int msf_to_frames(struct cdrom_msf0 * msf) {
return (msf->minute * 60 + msf->second) * 75 + msf->frame;
}
void frames_to_msf(int frames, struct cdrom_msf0 * msf) {
msf->frame = frames % 75;
msf->second = (frames / 75) % 60;
msf->minute = (frames / 75) / 60;
}
int main (int argc, char ** argv) {
int cdd, fd, track, start, stop;
char buf[BUF_SI
ZE];
struct cdrom_tochdr toc;
struct cdrom_tocentry entry;
struct cdrom_read_audio rdaudio;
struct wav_header hdr;
if (argc != 3) {
printf("использование: %s <трек> <имя файла>\n", argv[0]);
return 1;
}
cdd = open("/dev/cdrom", O_RDONLY|O_NONBLOCK);
track
= atoi(argv[1]);
ioctl(cdd, CDROMREADTOCHDR, &toc);
if ((track < toc.cdth_trk0) || (track > toc.cdth_trk1)) {
close(cdd);
printf("Неверный номер трека\n");
return 1;
}
entry.cdte_format = CDROM_MSF;
entry.cdte_track = track;
ioctl(cdd, CDROMREADTOCENTRY,
&entry);
if ((entry.cdte_ctrl & CDROM_DATA_TRACK) != 0) {
close(cdd);
printf("Трек не содержит аудиоданных\n");
return 1;
}
start = msf_to_frames(&entry.cdte_addr.msf);
entry.cdte_track = (track < toc.cdth_trk1) ? track + 1 : CDROM_LEADOUT;
ioctl(cdd, CDRO
MREADTOCENTRY, &entry);
stop = msf_to_frames(&entry.cdte_addr.msf);
fd = open(argv[2], O_WRONLY|O_CREAT, 0777);
memcpy(hdr.riff , (const void *) "RIFF", 4);
memcpy(hdr.rifftype, (const void *) "WAVE", 4);
memcpy(hdr.chunk_id1, (const void *) "fmt ", 4);
hdr.chunksize1 = 16;
hdr.wFormatTag = 1; // WAVE_FORMAT_PCM;
memcpy(hdr.chunk_id2, (const void *) "data", 4);
hdr.nChannels = 2;
hdr.nSamplesPerSec = 44100;
hdr.nBlockAlign = 4;
hdr.nAvgBytesPerSec = 44100 * hdr.nBlockAlign;
hdr.wBitsPerSample = 16;
hdr.chunksize2 = (stop-start)*2352;
hdr.filesize = hdr.chunksize2 + 44;
write(fd, &hdr, sizeof(hdr));
rdaudio.addr_format = CDROM_MSF;
rdaudio.buf = buf;
while (start < stop) {
frames_to_msf(start, &rdaudio.addr.msf);
start += (rdaudio.nframes = (stop - start) > 75 ? 75 : stop - start);
ioctl(cdd, CDROMREADAUDIO, &rdaudio);
write(fd, buf, rdaudio.nframes*2352);
}
close(cdd);
close(fd);
}

Первый параметр программы - номер трека, который следует записать. Второй параметр - имя файла, в котором сохраняются аудиоданные. Одной из проблем подобных программ при использовании очень дешевых (и не очень качественных) устройств чтения CD-ROM может стать неточность позиционирования. Профессиональные рипперы решают эту проблему чтением данных "внахлест" с последующем удалением избыточных фрагментов.

Форматы сжатия аудиоданных

Одна минута аудиозаписи с параметрами CD-DA занимает на диске около 10 мегабайт. Если для оптических носителей большие объемы информации не представляют серьезной проблемы, то при хранении записей на жестком диске и, тем более, при передаче аудиоданных через интернет, возникает необходимость сжатия данных. При этом, природа оцифрованного аудио-сигнала такова, что алгоритмы сжатия данных общего назначения оказываются сравнительно малоэффективными.

В распространенных в настоящее время форматах сжатия аудиоданных, обеспечивающих коэффициент сжатия приблизительно 1:10, применяются алгоритмы, относящиеся к категории методов сжатия с потерями. Как правило эти алгоритмы основаны на психо-акустике. Идеи, используемые в психо-акустических алгоритмах можно сформулировать в двух принципах:

Обратите внимание на слова "скорее всего". Дело в том, что психо-акустика ориентируется на "среднестатистическое человеческое ухо". Кроме того, ради увеличения коэффициента сжатия порой приходится жертвовать и различимыми звуковыми эффектами. По этому в мире сжатия аудиоданных мы сталкиваемся с теми же ножницами "коэффициент сжатия - качество", что и в сфере сжатия видео.

Далее мы рассмотрим подробнее два формата сжатия цифрового аудио: популярный MP3 и многообещающий Ogg Vorbis.

Формат MP3

Формат MP3, представляющий собой ответвление от могучего дерева мультимедиа-формата MPEG, получил чрезвычайно широкое распространение во всех сферах звуковоспроизведения, начиная с программ типа WinAmp и заканчивая специализированными плеерами со встроенными жесткими дисками. Не секрет, что совей популярностью MP3 обязан системам распространения записей через интернет, таким как Napster и Gnutella. Именно в Сети проявляются основные преимущества MP3 - высокая степень сжатия при сохранении отличного качества звука.

Для создания программы-проигрывателя MP3 файлов в системе Linux можно воспользоваться библиотекой smpeg, разрабатывающейся программистами из Loki Software, и предназначенной для воспроизведения различных MPEG форматов. Между прочим, эта библиотека не ориентирована исключительно на Linux, с ее исходными текстами поставляются файлы проекта MS Visual C++, так что приложения, использующие smpeg, могут быть кросс-платформеными.

Библиотека smpeg предоставляет интерфейсы как в виде набора классов C++, так и в виде набора функций для работы в C. Библиотека не отягощена документацией, что однако, не создает особых проблем, так как работать с smpeg не просто, а очень просто. Ниже приводится пример программы, написанной на C++. Несмотря на краткость листинга, эта программа - настоящий проигрыватель MP3 файлов.

#include <stdio.h>
#include <unistd.h>
#include <smpeg/MPEG.h>
#include <smpeg/MPEGaction.h>
int main(int argc, char * argv[]) {
if (argc !
= 2) {
printf("использование: %s mp3-файл\n", argv[0]);
return 0;
}
MPEG_AudioInfo ainfo;
MPEG * mpeg = new MPEG(argv[1]);
if (!mpeg->AudioEnabled()) {
printf("Нет аудиоданных\n");
return 1;
}
mpeg->Volume(100);
mpeg->GetAudioInfo(&ainfo);
printf("Версия:
%i \nслой: %i \nчастота дискретизации: %i Гц \nпоток данных: %i Кбит/сек\n", ainfo.mpegversion, ainfo.layer, ainfo.frequency, ainfo.bitrate);
mpeg->Play();
while (mpeg->Status() == (MPEGstatus) MPEG_PLAYING)
sleep(1);
delete mpeg;
return 0;
}

Строка для компиляции этой программы должна выглядеть следующим образом (исходный файл - mp3play.cpp):

gcc mp3play.cpp -lsmpeg -I/usr/include/SDL

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

Если вам необходим больший контроль над процессом воспроизведения, или вы хотите получить непосредственный доступ к декодированному цифровому потоку, можно воспользоваться функцией SMPEG_playAudio, позволяющей считывать декодированные данные из потока MP3 примерно также, как функция read считывает данные из потока ввода. Следующая программа, написанная на C, делает то же, что и предыдущая, но воспроизведение выполняется непосредственно драйверами OSS/ALSA, минуя промежуточный уровень SDL.

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <smpeg/smpeg.h>
#include <SDL/SDL_audio.h>
#include <sys/soundcard.h>
#include <sys
/ioctl.h>
#include <string.h>
#define BUF_SIZE 0x8000
int main(int argc, char * argv[]) {
Uint8 buf[BUF_SIZE];
SMPEG_Info sinfo;
SDL_AudioSpec as;
SMPEG* smpeg;
int len, fmt, audio_fd;
// создаем объект SMPEG с отключенным SDL
smpeg = SMPEG_new(argv[1], &
sinfo, SDL_FALSE);
if (sinfo.has_audio == SDL_FALSE) {
printf("Поток не содержит аудиоданных\n");
SMPEG_delete(smpeg);
return 1;
}
audio_fd= open("/dev/dsp", O_WRONLY);
fmt = AFMT_S16_LE;
ioctl(audio_fd, SNDCTL_DSP_SETFMT, &fmt);
SMPEG_wantedSpec(smpeg, &
as);
ioctl(audio_fd, SNDCTL_DSP_CHANNELS, &as.channels);
ioctl(audio_fd, SNDCTL_DSP_SPEED, &as.freq);
SMPEG_enablevideo(smpeg, SDL_FALSE);
SMPEG_play(smpeg);
memset(buf, 0, BUF_SIZE);
while ((len = SMPEG_playAudio(smpeg, buf, BUF_SIZE)) > 0) {
write(audio_fd, buf, len);
memset(buf, 0, len);
}
SMPEG_delete(smpeg);
close(audio_fd);
}

Обратите внимание на заполнение буфера нулевыми значениями перед каждым вызовом SMPEG_playAudio. Дело в том, что функция SMPEG_playAudio предполагает, что в переданном ей буфере уже содержатся какие-то аудиоданные, и не просто заполняет буфер своими значениями, а микширует их с уже имеющимися. Эта возможность оказывается полезной, например, при программировании игр, когда специальный звуковой сигнал, скажем, звук выстрела, накладывается на фоновую музыкальную тему, хранящуюся в MP3 файле.

Формат Ogg Vorbis

Главная проблема, связанная с форматом MP3, лежит не в технической, а в юридической плоскости. Дело в том, что формат MP3 запатентован (основной держатель патентов - германский институт Fraunhofer IIS). Политика правообладателей в области лицензирования и размеры лицензионных отчислений не отличаются особой последовательностью, что, естественно, затрудняет развитие бесплатных и открытых проектов, связанных с MP3. Подобная ситуация не могла не вызвать раздражения у сторонников открытых программ, и несколько лет назад группа программистов объявила о начале работы над новым форматом сжатия аудиоданных. Новая технология сжатия не только должна была превзойти MP3 по техническим характеристикам. Главным отличием формата, получившего загадочное название Ogg Vorbis, стала полная открытость и бесплатность. Формат Ogg Vorbis разрабатывается компанией Xiphophorus (www.xiph.org) в рамках весьма амбициозного проекта, призванного стать бесплатной альтернативой семейству форматов MPEG. Нельзя не отдать должное команде программистов из Xiphophorus - разработка Ogg Vorbis ведется стремительными, для столь масштабного проекта, темпами. В настоящее время кодек Ogg Vorbis все еще находится на стадии release-candidate, но сам формат уже вполне стабилен, и многие программы-проигрыватели поддерживают воспроизведение файлов с расширением .ogg.

Новейшие технологии в области качественного сжатия звука, такие как психо-акустика, сопряжение каналов (coupling), переменный поток данных (variable bitrate) позволяют формату Ogg Vorbis успешно конкурировать с такими признанными фаворитами, как MP3 и WMA.

Для работы с форматом Ogg Vorbis вам потребуются библиотеки кодека, которые можно загрузить с сайта компании Xiphophorus. Документация по API все еще неполна, однако вместе с кодеком поставляются подробно прокомментированные примеры, в том числе пример программы-кодировщика и полноценного плеера ogg123.

Далее мы рассмотрим несколько упрощенный пример плеера Ogg Vorbis.

#include <stdio.h>
#include <stdlib.h>
#include <vorbis/codec.h>
#include <vorbis/vorbisfile.h>
#include <sys/ioctl.h>
#include <sys/fcntl.h>
#include <linux/soundcard.h>
char buf[4096];
int main(int argc, char *argv[]){
FILE * f;
int fd, len, fmt;
Ogg
Vorbis_File vf;
vorbis_info *vi;
int current_section;
char **ptr;
if (argc == 1) return(0);
f = fopen(argv[1], "r");
if(ov_open(f, &vf, NULL, 0) < 0) {
printf("Формат не опознан.\n");
fclose(f);
exit(1);
}
printf("Кодировщик: %s\n", ov_comment(&vf, -1)->
vendor);
// Распечатываем содержащиеся в файле комментарии
ptr = ov_comment(&vf, -1)->user_comments;
while(*ptr){
printf("%s\n",*ptr);
++ptr;
}
vi = ov_info(&vf,-1);
fd = open("/dev/dsp0", O_WRONLY);
fmt = AFMT_S16_LE;
ioctl(fd, SNDCTL_DSP_SETFMT, &fmt);
i
octl(fd, SNDCTL_DSP_CHANNELS, &(vi->channels));
ioctl(fd, SNDCTL_DSP_SPEED, &(vi->rate));
while((len = ov_read(&vf, buf, sizeof(buf), 0, 2, 1, &current_section)) != 0) write(fd, buf, len);
close(fd);
ov_clear(&vf);
return(0);
}

Обратите внимание, что мы не закрываем файл f функцией fclose. После вызова функции ov_open, структура vf получает указатель на файл f. Файл закрывается в процессе вызова функции ov_clear.

Итак, хотя кодек Ogg Vorbis не достиг еще финальной стадии, мы можем ожидать, что новый формат займет достойное место среди своих коммерческих собратьев.

Примечание: Вторая версия игры Serious Sam использует кодек Ogg Vorbis для музыкального сопровождения. Это уже серьезно! :-)


Статья была опубликована в журнале "Программист", #3, 2002 г. Перепечатка статьи возможна только с разрешения редакции журнала.