Как работает интернет-радиовещание, часть 1

в 6:35, , рубрики: arecord, icecast, icy, IT-стандарты, mp3, shoutcast, ultravox, звук, интернет-радио, кодеки, Работа со звуком, радиовещание, Сетевые технологии, метки: , , , , , , , , , ,

Смотрите на любом IT-ресурсе страны

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

Что же такое звук?

Звук — это набор колебаний. Если быстро-быстро вибрировать, то будет звук, да еще какой! Сначала АЦП на вашем устройстве (аудиокарта, USB-микрофон, вебкамера) получает аналоговый сигнал, замеряет его значение в некий момент времени (громкость) и округляет до ближайшего дискретного значения, получая на выходе нужную нам цифру, с которой мы и будем работать. Каждое такое значение в отдельный момент времени — это и есть те самые колебания, применительно к звуку — семплы. За один момент времени может быть получено несколько таких семплов, по 1 семплу на канал. Скажем, если мы записываем звук в стерео, то будем получать по 2 семпла за одно время — фрейм.

Вот пример этих самых колебаний:
PCM

Подробнее о том, что же такое звук, о том как оно преобзуется в 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-плагины и хосты для них, но я так ничего толкового и не нашел. В обоих случаях не помешает мониторилка сигнала, дабы точно видеть, что именно приходит от наших железок/остается после процессинга.

Как работает интернет радиовещание, часть 1
Вот так это выглядит у меня — 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 (ныне покойное) и куча других проприетарных или недоделанных протоколов. Кодеки и протоколы различаются, а принцип один.

Работа над ошибками.

Радио то мы запустили, но есть ряд косяков:

  1. Файлы растут, удалить никак, место на сервере кончается!
  2. Проигрывание начинается всегда с одного места. Приветственную речь начальника конечно услышат все, что в общем-то неплохо, но его приказы дойдут до подчиненных с некоторым запозданием, к примеру, уже после их увольнения.
  3. Иногда проигрывание завершается и приходиться еще раз слушать все с самого начала!
  4. Голос начальника заменяется какими-то другими голосами, как узнать кто это?

Первую проблему пока обойдем стороной, винты нынче недорогие, можно парочку со склада выписать и сделать страйп, а вот с другими будем бороться. Дабы не играть все с самого начала, будем отдавать файл с конца, вот коротенький 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


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js