Радиовещание — это такая странная и загадочная штука, которая возволяет передать звук за тысячи километров в уши благодарных слушателей, управляя их настроением и поведением. История радиовещания сама по себе очень интересна, равно как и все последствия, которые оно за собой принесло. Но историю лучше почитать в википедии, а в этой небольшой статье будет рассказано, как же происходит работа интернет-радио. Здесь не будет мануалов по настройке, здесь не будет конфигов, зато будет много картинок и немного кода.
Что же такое звук?
Звук — это набор колебаний. Если быстро-быстро вибрировать, то будет звук, да еще какой! Сначала АЦП на вашем устройстве (аудиокарта, USB-микрофон, вебкамера) получает аналоговый сигнал, замеряет его значение в некий момент времени (громкость) и округляет до ближайшего дискретного значения, получая на выходе нужную нам цифру, с которой мы и будем работать. Каждое такое значение в отдельный момент времени — это и есть те самые колебания, применительно к звуку — семплы. За один момент времени может быть получено несколько таких семплов, по 1 семплу на канал. Скажем, если мы записываем звук в стерео, то будем получать по 2 семпла за одно время — фрейм.
Вот пример этих самых колебаний:
Подробнее о том, что же такое звук, о том как оно преобзуется в PCM, можно узнать на:
www.alsa-project.org/alsa-doc/alsa-lib/pcm.html
en.wikipedia.org/wiki/Pulse-code_modulation
Вот в принципе и все, что нам нужно знать, дабы начать работать с ним!
Больше практики!
Дабы не превращать статью во что-то унылое, то можно начать наши маленькие эксперименты. Если звук одна из главных составляющих частей радиовещания — надо научиться его добывать! Обладатели Linux могут написать:
arecord > file.wav
И получат играбельный файл, содержащий данные с дефолтного устройства (микрофона, системные звуки — зависит от настроек микшера). Как многие уже догадались, wav — это и есть набор тех самых колебаний, записанных в файл (в большинстве случаев, но иногда там оказывается что-то совсем далекое от семплов).
К сожалению, я не смог найти свободного аналога под win-платформу, все найденное — платное/проприетарное и с кучей ненужных функций. Может подскажете что-то похожее на arecord?
Но если послушать этот файл, то качество будет не самым лучшим — по умолчанию 8 килогерц, моно, 8 бит. Немного улучшим качество:
arecord -fcd -r44100 -c2 > file.wav
Теперь наш файл звучит гораздо лучше, ведь в нем уже не 8000 колебаний в секунду (по умолчанию), а уже 44100, не один канал, а целых 2 (тем самым в секунду сохраняется 88200 семплов с обоих каналов), да и семплы пишутся уже в 16 битах.
Чем их захватить этих самых семплов (колебаний) за определенный интервал времени — тем более плавный и «живой» звук, способный передать больше высоких частот (скрипка, пение птиц), чем меньше — тем больше напоминает звук жестянки и голоса роботов из фильмов прошлого века. Не стоит забывать и о качестве этих колебаний — амплитуде, чем меньше шаг, тем более «чистый» звук, чем больше — тем больше отклонений от оригинальной формы сигнала, «несвоевременных» колебаний, которые расцениваются ухом как посторонний шум.
Известное многим телефонное качество звука — это 8 килогерц, или 8000 таких колебаний в секунду. На CD-дисках, то самое «лазерное качество», которое ms предлагает жать в 96 килобит, это уже 44100 таких колебаний в секунду. Колебания звука зачастую представлены в виде signed short или float значений. Современная аппаратура может воспроизвести и больше колебаний, но лично мое ухо уже не слышит разницы. К сожалению, я нищий, поэтому не могу покупать провода из золота и специальные аудиосистемы размером с дом, дабы проверить все на собственном опыте.
Улучшим
Уже сейчас можно окунуться в чудеснейший мир LADSPA/VST-плагинов: поправить поведение некоторых микрофонов, которые играют только в один канал, добавить громкости, возможно простые эффекты вроде реверберации, даже сделать свой голос похожим на голоса из мультипликационных фильмов. Загляните в LADSPA-директорию в вашем дистрибутиве — вас ждет большой сюрприз — от простых гейтов до продвинутых эквалайзеров и подавителей шума. Можно делать пайпы в sox и другие аудиопроцессоры. Пользователи win-платформы могут поискать VST-плагины и хосты для них, но я так ничего толкового и не нашел. В обоих случаях не помешает мониторилка сигнала, дабы точно видеть, что именно приходит от наших железок/остается после процессинга.
Вот так это выглядит у меня — 2 монитора и нормализатор в середине. Сразу видно что пришло, а что ушло после процессинга. Запуск соответственно примерно такой:
arecord -fcd -r44100 -c2 | viewSignal | sox .... | other_processing | ... | viewSignal > file.wav
В любом случае, мы научились сохранять файл, причем делать это хорошо. В принципе, радиовещание — это передача звука, звук — это информация, а чем мы обычно в интернете передаем информацию? Обычно через протокол http, за который отвечает вебсервер, значит можно сохранять этот файлик в директорию веб-сервера, а на клиенте играть примерно так:
mplayer http://radio.example.tld/file.wav
vlc http://radio.example.tld/file.wav
Наше радио работает! В принципе, почти все хорошо, можно запускать в продакшен, однако… Слишком уж много оно весит… Где взять трафик на все это?
Сжатие. А сколько это в граммах?
Ну давайте посчитаем. Скажем, пусть на основу у нас будет 10 минут в 44.1kHz стерео, семплы которого сохранены в signed short (2 байта), тогда:
10*60*44100*2*2=105 840 000 байт за 10 минут.
Что-то как-то много выходит… Это ж сколько дискет надо, дабы песенку записать?
Телефонистам немного проще, у них 8 килогерц моно, 1 байт на семпл:
10*60*8000*1*1=4 800 000 байт
Почти в 20 раз меньше! Дело в том, что телефонисты исхитрились используя логарифмическое кодирование alaw или ulaw, тем самым сумев затолкать достаточно большой диапазон значений всего в 1 байт (надо сказать, что в один байт можно затолкать и больше, скажем используя ADPCM: en.wikipedia.org/wiki/Adaptive_differential_pulse-code_modulation, но так как эта статья не о телефонии, то писать про современные телефонные кодеки нет смысла, равно как и про экспериментальные, такие как codec2 с его сверхнизкими 1.5 килобитами, которые сокращают объем данных примерно в 1000 раз).
Вот так, несколько издалека, но мы невольно подобрались к теме компрессии. Данных много и их надо как-то сжать. Звук можно сжать как без потерь, скажем в gzip или flac, так и с потерями — vorbis, opus, aac+. Компрессоров обоих типов огромное множество, причем как специализированных, так и без специальной адаптации для звука. Но пожалуй самым первым популярным методом компрессии, который «пошел в народ», стал mp3. И действительно, поток в 128000 бит/сек оставляет звук вполне слушабельным, при этом снижает поток битов примерно в 11 раз, что уже достаточно приемлемо.
Обычно mp3-файл состоит из набора фреймов, в начале и конце которого могут быть теги. Единого заголовка в файле нет, зато у каждого фрейма есть свой заголовок с сигнатурой для синхронизации. Это позволяет, с одной стороны, повышать живучесть файла, преодолевая его повреждения, а с другой стороны, невозбранно его кромсать. Такое знание отрывает путь к магии. Когда моим знакомым надо отрезать кусок многочасовой mp3-записи, они запускают саундфорж и мучаются в нем, а я же использую dd и раза с 3-4 вырезаю нужный кусок (да, я в курсе о существовании утилит для нарезки mp3 без рекомпрессии, но нарезать ими 500-метровые записи долго). Когда знакомым надо сделать склейку из нескольких файлов, то они опять мучают саундфорж, а я делаю cat file1.mp3 file2.mp3 file3.mp3 > megamix.mp3 — все работает безупречно. Конечно, перфекционисты меня закидают помидорами, но в общем случае это не вызывает проблем, поэтому дальше мы тоже будем нещадно кромсать mp3, делая процесс более наглядным, не останавливаясь на подробностях работы с контейнерами. Да, контейнер mpg (program stream) и уж тем более ts (mpeg transport stream) тоже обладают такой живучестью. А вы монтируете фильмы при помощи dd?
Практика
arecord -fcd -r44100 -c2 | lame -x -r -s 44.1 --bitwidth 16 -m j - - > file.mp3
На выходе получаем mp3-файл. В качестве кодировщика мы тут используем lame, для большей наглядности идет редирект в файл. Обратите внимание, здесь используется связка -x -r, возможно у кого-то из-за этого будет шум и вам придется поколдовать с опциями. А может быть вы вообще будете использовать что-то другое — не принципиально. Это просто пример.
Итак, запускаем наш комбайн:
arecord -fcd -r44100 -c2 | viewSignal | sox .... | other_processing | ... | viewSignal | lame -x -r -s 44.1 --bitwidth 16 -m j - - > ~/server/web/file.mp3 (сохраняем в директорию вебсервера)
И можно попробовать слушать наше радио командой:
mplayer http://radio.example.tld/file.mp3
vlc http://radio.example.tld/file.mp3
Все, теперь канал не пожирается, музыка играет, клиенты будут довольны, а начальство выпишет премию.
Примерно по этому принципу работают те 50000 радиостанций, которые можно найти на www.shoutcast.com/
Если кто-то не понял: да, интернет-радио в этом случае «просто mp3-файлик, который скачивается во время проигрывания».
Существует и много других способов радиовещания в интернет, но все они сводятся к доставке «длинного файла со звуком клиенту, да побыстрее», к примеру: торренты, RTMP, RTSP (RTP), MMS (ныне покойное) и куча других проприетарных или недоделанных протоколов. Кодеки и протоколы различаются, а принцип один.
Работа над ошибками.
Радио то мы запустили, но есть ряд косяков:
- Файлы растут, удалить никак, место на сервере кончается!
- Проигрывание начинается всегда с одного места. Приветственную речь начальника конечно услышат все, что в общем-то неплохо, но его приказы дойдут до подчиненных с некоторым запозданием, к примеру, уже после их увольнения.
- Иногда проигрывание завершается и приходиться еще раз слушать все с самого начала!
- Голос начальника заменяется какими-то другими голосами, как узнать кто это?
Первую проблему пока обойдем стороной, винты нынче недорогие, можно парочку со склада выписать и сделать страйп, а вот с другими будем бороться. Дабы не играть все с самого начала, будем отдавать файл с конца, вот коротенький CGI-скрипт:
#!/usr/bin/perl
$|++;
use strict;
print "Content-Type: audio/mpegnn"; # Изображаем вебсервер и говорим, чего будем отдавать
open(mpeg,"file.mp3"); # открываем наш файл на чтение
binmode(mpeg); # Дабы у пользователей win-платформы тоже все работало
seek(mpeg,-65536,2); # Перемарываем файл в конец, оставляя буфер в 65кб.
my $buf; # Переменная под буфер
while(1){
if(read(mpeg,$buf,4096)>0){ # Если мы что-то смогли прочитать из файла...
print STDOUT $buf; # ... то отправляем это клиенту ...
} else { # ... а иначе ...
seek(mpeg,0,1); # перепозиционируемся, дабы снять флаг окончания файла
sleep 1; # и проспим 1 секунду в ожидании, когда же наш файл пополнится.
}
}
Перематываением мы исправляем проблему №2 — проигрывание начнется с самого последнего момента, 65кб — это размер буфера во многих плеерах, именно столько данных они должны прочитать для начала воспроизведения, о точной позиции файла мы можем не беспокоиться — MP3-стрим толерантен к ошибкам, а ожиданием пополнения файла — решаем проблему №3, таким образом поток никогда не закончится (даже если файл больше не писать). Конечно, код представлен сугубо в образовательных целях, всю ответственность за его использование вы берете на себя.
В принципе, можно получать премию… У нас рабочее решение для дистрибьюции контента!
А кто это говорит?
Даже FM-станции научились передавать название играющей композиции, а уж передать нам имя говорящего начальника в тегах — дело святое. Передача метаданных — штука крайне интересная. В некоторых контейнерах, таких как OGG, предусмотрены пакеты/структуры для метаданных, а раз они внутри основного стрима, то и проблем с пересылкой тегов не возникает, все просто. Но у нас то mp3! Можно конечно инкапсулировать mp3 в какой-то контейнер, скажем FLV и в него подмешивать метаданные, но кто это будет слушать? Много ли аудиоплееров понимает FLV? Людям нужен чистый mp3! Можно конечно в самом начале воткнуть ID3v2.3.0-тег, плеер такое прочитает, но как быть с обновлением? Тег может быть только в самом начале. Рвать соединение? В принципе, это вариант: можно пойти по пути Apple — сделать m3u-файл, внутри которого пустить ссылки на нарезку, а уже внутри нарезки будут теги:
developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/StreamingMediaGuide/UsingHTTPLiveStreaming/UsingHTTPLiveStreaming.html
Однако авторы Winamp, компания Nullsoft, решила создать еще один транспортный уровень — ICY, о котором и поговорим.
ICY
Протокол ICY очень похож на http, но с некоторыми отличиями. Некоторые его считают за «http c костылями», ругаются на проблемы проксирования/кеширования. Давайте рассмотрим:
Запрос потока напоминает стандартный http-запрос:
GET /anyshit HTTP/1.0
Host: radio.example.com
Icy-MetaData:1 <---- магия
[пустая строка]
Если на той стороне простой вебсервер — он не поймет смысл заголовка Icy-MetaData, клиент получит просто mp3 и будет играть его как обычный файл. С тегами все как и в случае с обычным файлом. Если же сервер знает про Icy-MetaData, то в ответ он отдаст заголовок вида icy-metaint:8192, что обозначает: «через каждые 8192 байта контента будет поле метаданных», т.е. сам контент будет нарезан на кусочки в 8192 байта, между которыми будут метаданные.
Метаданные — это 1 байт, который представляет собой размер строки метаданных (умножить на 16 для получения размера в байтах), которая идет непосредственно за этим байтом. К примеру:
StreamTitle='Сейчас говорит начальник транспортного цеха';
Если метаданных новых не поступало — в стриме идет просто 0, за ним сразу продолжается mp3-поток.
Подробно и в картинках можно прочитать здесь: www.smackfu.com/stuff/programming/shoutcast.html
Замечу, что многие плееры хотят ответ не HTTP/1.0 200 OK, а ICY 200 OK, вместо «Content-Type: » хотят «content-type:» и строгий порядок полей, который отдают shoutcast-сервера. Иначе могут быть всякие неприятности, мы вот патчили айскаст, дабы он эмулировал заголовки ICY. Ну и конечно, без ICY-ответа скорее всего останутся нераспознанными поля вида icy-name, icy-genre и т.п.
Протокол был одним из первых, который использовался для радиовещания в интернет, получил широкое распространение в плеерах и фактически остается стандартом де-факто и по сей день, но имеет ряд недостатков, самый яркий из которых — отсутствие указания кодировки. В старых версиях Winamp использовалась системная локаль и русские теги бегали в cp1251, нынче же там используется utf8, но по сей день можно встретить плееры, которые делают двойное преобразование к юникоду и показывают ÑÑÑÑкие ÑегРпримерно таким образом, хотя СЂСѓСЃСЃРєРёРµ теги иногда встречаются и в таком виде. Еще в нем иногда проскакивает метадата вида:
StreamTitle='Michael Bubl. - I.ll Be Home For Christmas';StreamUrl='&artist=Michael%20Bubl%E9&title=I%92ll%20Be%20Home%20For%20Christmas&album=Christmas&duration=266475&songtype=S&overlay=NO&buycd=&website=&picture=';
К практике!
Давайте улучшим наш скрипт, который будет отдавать поток с нашего сервера. Пусть теги у нас будут лежать в файле currentTitle, мы будем регулярно читать его, и если в нем что-то новое — мы добавим метаданные в отдаваемый поток.
#!/usr/bin/perl
$|++;
open(mpeg,"file.mp3"); # открываем наш файл на чтение
binmode(mpeg); # Дабы у пользователей win-платформы тоже все работало
seek(mpeg,-65536,2); # Перемарываем файл в конец, оставляя буфер в 65кб.
my($buf,$tmp); # Буфера для чтения файла
my($meta,$oldmeta); # Переменные под метаданные
my $metadata; # Будет содержать готовые метаданные
my $interval=8192; # Размер кусочков, на которые мы будем нарезать поток
print "icy-metaint:".$interval."n"; # Говорим клиенту, как часто будем нарезать...
print "content-type:audio/mpegnn"; # ... и собственно чего будем отдавать
while(1){
while(length($buf)<$interval){ # пока недостаточно данных для новгого пакета, то...
if(read(mpeg,$tmp,4096)>0){ # Если мы что-то смогли прочитать из файла...
$buf.=$tmp; # ... то дописываем это в буфер ...
} else { # ... а иначе ...
seek(mpeg,0,1); # перепозиционируемся, дабы снять флаг окончания файла
sleep(1); # ... проспим 1 секунду в ожидании, когда же наш файл пополнится
}
}
open(meta,"currentTitle");read(meta,$meta,-s(meta));close(meta); # Читаем метаданные
if($meta ne $oldmeta){ # Сравниваем с предыдущей записью, изменилось ли?
$metadata="StreamTitle='".$meta."';"; # Конструируем строку метаданных
my $padding=length($metadata)%16; # Так как размер метаданных указывается как
# множитель на 16, то следовательно нам надо
# отдать строчку. размер которой будет строго
# кратен 16 байтам. Разделим размер строки на
# 16 и получим остаток
$padding=$padding==0?0:16-$padding; # Ну и собственно посчитаем, сколько байт надо дополнить.
$metadata.="x00" x $padding; # Дополняем эти байты нулями
$oldmeta=$meta; # И сохраняем новые теги, дабы при следующей
# проверке ничего не выдавать
} else {
$metadata=""; # Т.е. как здесь - просто пустая строка, когда нет обновлений
}
# Отдаем: кусок данных, размер метаданных, сами метаданные.
print STDOUT substr($buf,0,$interval).pack("C",length($metadata)/16).$metadata;
$buf=substr($buf,$interval); # Отрезаем уже отданное.
}
Теперь мы не только можем слушать аудиопоток, но и видеть в плеере информацию о потоке. Теперь то уж точно будет видно, кто говорит в этот момент!
Ultravox 2.1
С годами требований было все больше, не добавлять же все новые и новые поля в StreamUrl? А как передавать обложки альбомов? И создатели популярного плеера изобрели:
wiki.winamp.com/wiki/SHOUTcast_2_(Ultravox_2.1)_Protocol_Details — замену ICY
wiki.winamp.com/wiki/SHOUTcast_XML_Metadata_Specification — сами теги, которые будут внутри.
Протокол уже имеет честные HTTP/1.1 200 OK, однако сам поток стал сложнее. Метаданные теперь поставляются в виде XML с кучей полей, указываются кодировки, прежним костылям пришел конец. Можно даже отсылать картинки. Забегая немного вперед хотелось бы отметить, что протокол используется не только для проигрывания, но и для вещания радиопотока, где можно передавать свои заставки, бекап-стримы, более точно контролировать метаданные, а пожалуй главное — можно контролировать целостность потока, Shoutcast-сервер точно знает, когда вещатель завершил свою работу, а когда просто отвалился из-за проблем с интернетом — это помогает обрабатывать ошибки, например, запуская бекап-стрим. Его коллега Icecast оба случая считает окончанием стрима, что порой приводит к некоторым проблемам…
Практики сейчас не будет, кроме самого Winamp-а я не знаю плееров, которые реализуют протокол Ultravox 2.1.
Может быть реализуете сами, в качестве домашнего задания для саморазвития? Равно как инкапсуляцию mp3 и метаданных в FLV, вдруг будет полезно?
Заключение
Все бы ничего, но винты кончаются, поэтому в следущей части мы найдем способ не хранить многогигабайтные mp3 на сервере, найдем более удобные средства для управления метаданными, нежели прописывание тегов в файл, переместимся в датацентр, научимся передавать mp3-поток туда и поговорим о сопутствующих проблемах.
Честно говоря, я не ожидал что вступление будет таким большим, поэтому решил разбить статью на несколько частей. И раз уж статья будет в нескольких частях, то может быть подскажете интересующие вас направления? Поделитесь своим опытом?
Автор: iFrolov