Pull to refresh

Comments 33

Тема крайне интересная, хотя мне вот все же сложно представить, что такое может работать без задержки в десятки миллисекунд. Дома обязательно протестирую :)
О, это то, что я когда-то давно искал! В GPL бы и на sourceforge, чтобы по-серьёзному использовать можно было.
Года три назад я общался с одним из разработчиков Musigy. Очень заинтересовала эта тема. Вот что он мне сказал:

Проблем было три:
а) непонятная / малая рыночная ниша
б) очень малое количество low-latency звуковых карт у потенциальных пользователей (сейчас с этим дела получше — у меня уже даже в гитарный процессор такая карта встроена)
и трудность в их настройке (обычно музыканты плохо разбираются в компах, а правильно настроить ASIO-звуковую карту бывает непросто)
в) жесткие требования на сетевую задержку, в т.ч. принципиально непреодолимые (скорость света)
Технически сделать аналог Musigy чтобы оно было non-profit программой для энтузиастов (без супер-суппорта и надежности, но чтобы хоть у кого-то работало) — не проблема.
Для меня эта тема очень интересна, будет сильно экономить время и деньги.
Готов поучавствовать в разработке версии под Mac OS X если проект станет открытым.
Постараемся потестить втроем на следующей неделе.
Странно, что не был упомянут проект ejamming.com, который существует с 2007 года — никакой задержки (зависит только от своей звуковухи), стереодорожки, возможность записи и т.д. Месяц бесплатно, потом $10/мес.

Еще из известных мне:
onlinejamsessions.com
www.musicianlink.com (девайс для этих задач)
llcon.sourceforge.net
www.soundjack.eu
А что значит отсутствие буфера?

Я правильно понимаю, что программа не тестировалась на не-локальных сетях, где есть jitter?
Неправильно. Программа была написана именно для того, чтобы играть через интернет. И она успешно с этим справилась.
Не хрипит / не задерживается ли звук при игре по wi-fi?

Какой длительности сессии?
Не происходит ли через 10-20 минут сессии рассинхронизации играющих?
Не хрипит / не задерживается ли звук при игре по wi-fi?

Отлично работает с древним 7-дюймовым нетбуком EEE по Wi-Fi.

Не происходит ли через 10-20 минут сессии рассинхронизации играющих?

Несколько более чем часовых сессий через интернет проходили без проблем.
Поясните пожалуйста, как именно оно работает без буферизации, когда нового звука от удалённого участника ещё не поступало?
Например, если произошёл packet loss или толпа пакетов оказалась в «пробке» на одном из узлов сети по дороге?

Какие расстояния подразумевается под словом «интернет» здесь:
чтобы играть через интернет. И она успешно с этим справилась
?

В частности, я сомневаюсь что
без этого буфера в Music Over The World Tool можно без проблем играть на темпе 120-140
между Москвой и Сан-Франциско.
как именно оно работает без буферизации, когда нового звука от удалённого участника ещё не поступало
Просто ASIO-драйвер callback'ом (44100 / buffSize) раз в секунду вызывает огроомную функцию, в которой написано в т.ч. следующее:

#define MAX_NET_USERS 8 // максимальное число удаленных участников музыкальной онлайн-сессии; в первом релизе сделать 8
for(int i = 0; i < MAX_NET_USERS; i++)
{
    // послать смикшированные сэмплы
    if(nSend[i] == 1)
    {
        NetSend((unsigned char*)(asioDriverInfo.bufferInfos[OutputCh].buffers[index]), (unsigned char*)(asioDriverInfo.bufferInfos[OutputCh+1].buffers[index]), buffSize, i);
    }
    // считать сетевые данные
    if(NetReceive((unsigned char*)(BigLeft), (unsigned char*)(BigRight), buffSize, i) == 0)
    {
        ClientCounter++;
    }
}

, где:

int NetReceive(unsigned char *Left, unsigned char *Right, int MaxSamples, int ClientIndex)
{
    //...
    if(NetReceive_real(ReceivedFrame, (2048 * 4 * 2) + 4 + 4, ClientIndex) == 0)
    {
        for(int NewBufPos = 0; NewBufPos < MaxSamples; NewBufPos++)
        {
            // смикшировать очередной сэмпл левого канала
            // смикшировать очередной сэмпл правого канала
            //...
        }
    }
    else
    { 
        //...
        return -1;
    }
return 0;
}

, где:

int NetReceive_real(unsigned char * data, int maxdatasize, int ClientIndex)
{
    struct sockaddr_in RecvAddr;
    int fromlen = sizeof(struct sockaddr_in);
    if(recvfrom(MySocket[ClientIndex], (LPSTR)data, maxdatasize, 0, (sockaddr*)&RecvAddr, &fromlen) >= 0)
    {
        PeerTableSetLastUpdateTime(RecvAddr.sin_addr.s_addr, MySystemTime);
        return 0; // если данные с сокета успешно считаны то возвращаем ноль
    }
    else
    {
        return -1; // иначе НАДО ВОЗВРАТИТЬ -1(!)
    }
}

.

Какие расстояния подразумевается под словом «интернет» здесь
Хм, один человек был в этом-же городе, где и я, и еще один был в соседнем городе за 50 км.
Сейчас спросил — друг, с которым тестировали, еще играл с городом, который находится за 800 км от него, говорит нормально. Сейчас он скинул мне записи и могу сказать, что проблем нет. Вот отрывок того файла.

В частности, я сомневаюсь что без этого буфера в Music Over The World Tool можно без проблем играть на темпе 120-140 между Москвой и Сан-Франциско.
Как я уже написал, чудес не бывает. Программа не может уменьшить пинг между двумя удаленными компьютерами. Так что если пинг будет миллисекунд 80, то проблемы будут. Но уж точно не по вине программы, суть то в этом.
как именно оно работает без буферизации, когда нового звука от удалённого участника ещё не поступало
Просто ASIO-драйвер callback'ом (44100 / buffSize) раз в секунду вызывает огроомную функцию, в которой написано в т.ч. следующее:
То есть если пакет пришёл — он микшируется со следующим звуковым фреймом на playback, иначе — вместо канала пустота, верно?

Без этого буфера в Music Over The World Tool можно без проблем играть на темпе 120-140.
Как я уже написал, чудес не бывает.
Всё понимаю, просто исходная формулировка показалась мне эээ неточной :)
То есть если пакет пришёл — он микшируется со следующим звуковым фреймом на playback, иначе — вместо канала пустота, верно
Верно. А разделение клиентов друг от друга осуществляется за счет разных портов приема. Причем тут тоже был интересный подводный камень. Чтобы в распределенной p2p-сети два разных участника не слали данные в один и тот же порт, каждый участник шлет данные в порт, индекс которого является позицией абсолютного четырёхбайтового значения своего ip-адреса в отсортированной по возрастанию таблице ip-адресов всех участников сессии.
Плохие новости: такой подход будет очень плохо работать при потерях пакетов и особенно при джиттере.

Если пакет в сети продолбается, то вместо него будет сеанс тишины длиной несколько миллисекунд. При этом в начале тишины и в конце тишины, судя по приведённому коду, будет резкий переход от «звук есть» к «звука нет» и обратно. Каждый такой переход — щелчок. Попробуйте послушать непрерывно звучащую долгую ноту (или просто белый шум) от другого участника, находясь через одну-две несущие стены от роутера :)

При джиттере дела обстоят ещё хуже. Если где-то по дороге произойдёт «пробка» пакетов, то вначале приведённый код выдаст несколько сеансов тишины, а потом прибежит кучка пакетов. Каждый пакет из очереди UDP-пакетов код пойдёт с радостью проигрывать, ни одного не пропуская.
Что это значит? Это значит что весь дальнейший звук удалённого участника будет задерживаться на временной размер произошедшей «пробки». И так — до тех пор, пока не потеряется достаточное количество пакетов, чтобы скомпенсировать эту задержку.
То, что буферизации нету в прикладной программе, вовсе не значит, что её нет например в самом сокете.
Вывод: буферизация в вашей системе уже есть, просто вы об этом не догадывались :)

Советую почитать что такое jitter-буфер и как он устроен в современных VoIP системах. Буферизация в подобных системах делается специально и ради очень важной части функциональности — осмысленно-контролируемом размере задержки (в противовес «как получится»).

Чтобы в распределенной p2p-сети два разных участника не слали данные в один и тот же порт, каждый участник шлет данные в порт, индекс которого является позицией абсолютного четырёхбайтового значения своего ip-адреса в отсортированной по возрастанию таблице ip-адресов всех участников сессии.
Что если два участника из трёх находятся в одной виртуальной подсети?
вначале приведённый код выдаст несколько сеансов тишины
весь дальнейший звук удалённого участника будет задерживаться
буферизация в вашей системе уже есть, просто вы об этом не догадывались
Да, как я уже говорил, у меня есть опыт работы с UDP, и это я знал. Но единственной мыслью было периодически очищать буфер сокета. Только запамятовал, можно ли получить статистику о количестве пришедших пакетов на сокет? Если можно, то может стоит очищать сокет, если скопилось больше одного пакета. Хм.

Что если два участника из трёх находятся в одной виртуальной подсети
А можно на примере, не совсем понятно что такое «виртуальная подсеть» в данном контексте?
Но единственной мыслью было периодически очищать буфер сокета.
Каждый раз когда при воспроизведении будет пропускаться хотя бы один пакет (очищенный), будет происходить негладкая склейка звука, то есть щелчок.

Только запамятовал, можно ли получить статистику о количестве пришедших пакетов на сокет?
Честно скажу — не знаю :) И узнавать не советую, так как всё равно по вышеописанным причинам (склейка, пропуск пакетов, потери пакетов) нужно будет делать нормальный jitter-буфер, читающий из сокета в неблокирующемся режиме. И кстати там можно и multiplexing будет спокойно сделать через один порт.

А можно на примере, не совсем понятно что такое «виртуальная подсеть» в данном контексте?
Например, если они вдвоём сидят за одним NAT'ом и получают один и тот же публичный IP-адрес.
всё равно по вышеописанным причинам (склейка, пропуск пакетов, потери пакетов) нужно будет делать нормальный jitter-буфер
Все правильно, можно улучшить эту часть в моей программе. Но, по-первых, у меня проблем с потерями пакетов не возникало. Во-вторых, в комментариях выше были указаны замечательные ссылки на программы Jamulus и Soundjack, о которых я не знал. Судя по описанию это как раз то, что нужно. В случае проблем с моей программой буду использовать их.

Например, если они вдвоём сидят за одним NAT'ом и получают один и тот же публичный IP-адрес.
В лучшем случае первый подключившийся из виртуальной сети к хосту во внешней сети будет передавать свои аудио данные, а сам слышать ничего не будет. Ибо с NAT'ом я не разбирался и ничего специально для этого не писал.
Все правильно, можно улучшить эту часть в моей программе. Но, по-первых, у меня проблем с потерями пакетов не возникало.
Понятно. Другими словами, озвученное
главное отличие программы Music Over The World Tool от Netduetto
оказалось не супер-преимуществом
А что значит отсутствие буфера? Это значит что собеседник услышит ваш аккорд на несколько миллисекунд раньше.
, а скорее недодуманностью/недоработкой в силу недостаточного тестирования. Бывает.

Щелчки это вообще штука коварная. Пока их нет — всё хорошо и кажется что ну и фиг с ними. А вот если на дорогом/мощном оборудовании получить негладкую склейку звука от -1.0 к +1.0 (т.е. от минимума до максимума; или наоборот), да ещё и несколько раз подряд, слушателям мало не покажется. Например, при концерте через подобную систему. Да и оборудование может повредиться.
Понятно, что всё это явления вероятностные (и вероятность поймать склейку именно -1… 1 довольно низка), но раз в год и палка стреляет.

Кстати, готов поспорить что слепое сравнение задержки 20мс (без специальной буферизации) и 23-25мс (с нормальным jitter-буфером и нулевой вероятностью щелчка) не покажет сколь-либо существенной разницы (если вообще покажет хоть какую-то).

В лучшем случае первый подключившийся из виртуальной сети к хосту во внешней сети будет передавать свои аудио данные, а сам слышать ничего не будет.
Не-не, в лучшем случае всё будет работать ОК. А вот в худшем — два клиента за NAT будут посылать пакеты в один и тот же порт «нормального» третьего участника. Получится что-то типа dubstep :)
Понятно. Другими словами, озвученное оказалось не супер-преимуществом, а скорее недодуманностью/недоработкой.
Не совсем так. Во-первых, я нигде не писал об этом как о «супер-преимуществе», я лишь описал это в списке вещей, «которые мне показались интересными в процессе разработки программы». Даже вообще не называл это преимуществом, говоря «отличие в плане буферизации». Во-вторых, при качественном интернете, или в локальной сети, это действительно преимущество, а не недоработка. Ибо реально минимизирует задержку.

Но, в итоге да. Вы убедили меня в том, что моя программа очень нишевая — годится для локальных сетей и репетиций (не выступлений) в рамках одного провайдера.
Если затачиваться на такую нишу, то можно оптимизировать jitter-буфер так, чтобы он не вносил никакой задержки до тех пор, пока сеть не «штормит». На репетиции тоже можно сделать ушам больно / сжечь что-нибудь.
Тимур, да что вы так переживаете за щелчки и сжигание аппаратуры? :) Есть же куча способов этого избежать. Потерялся пакет, не успел прийти — ну и хрен с ним! Берем предыдущий и миксуем с текущим аккуратно, чтобы без щелчков, можно даже алгоритмически фазу сохранить. Таким образом умно «продлим» текущую ноту. А насчет щелчков и сжигания — просто не позволять резкие переходы, сглаживать их опять же алгоритмически. Повесить функцию лимитера пиков. При внезапной потере пакета, в котором образуется ситуация близкая к +1.0/-1.0 в соседних семплах — делаем «плавное погашение», например, с частотой основного тона за последние пару мс. В общем, если подумать — много способов обойти эту проблему, не вводя дополнительного буфера, который внесет очередную задержку, что при игре по сети сводит на нет задумку. Ведь при задержка более 10мс уже ощутима, а при >50мс играть становится весьма сложно.

Насчет джиттера — присваивать каждому пакету временную метку. Не уложился в указанные сроки — отрезается начало вэйва, которое уже должно было прозвучать, доигрывается та часть из него, которая еще «в тему». Если совсем не успел — пропускаем, алгоритмически заменяя его на что-нибудь, как предложено в вариантах выше.
Всё вышеперечисленное прекрасно понимаю, просто хотелось чтобы автор сам до этого догадался :)

а при >50мс играть становится весьма сложно.
На мой вкус, даже при 30-35 играть становится сложно. 50мс это разве что для медленного блюза…
Эх, такое бы под Linux или Mac… У меня в коллективе есть несколько музыкантов из Москвы, с которыми я уже год не могу сыграться :(
Конечно, чудес не бывает, и за все приходится платить — нужно устанавливать на всех компьютерах участников одинаковый размер входных и выходных буферов ASIO-драйверов.
Это необязательное ограничение.

Рассмотрим случай двух участников. Если один из размеров буферов участников кратен размеру буфера другого (что часто бывает с ASIO, например 128 vs 256), то один может посылать свои 256 и они спокойно могут быть разобраны на два фрейма тем, у кого 128 — без внесения дополнительной задержки. Тот, который принимает по 128, а хочет по 256 — аналогично, вполне может группировать чужие фреймы по два.

Если подумать ещё чуть-чуть, то приходящие фреймы можно просто склеивать в очередь с операцией «верни мне N сэмплов или false», и тогда убирается ограничение на кратность. При этом можно заметить, что раз нам не нужна кратность, то без ограничения общности оно будет работать и с M участников.
Хм. Пост навел на мысли о том, что получается, что при разных размерах ASIO-драйверов у разных участников сессии получается разный user-experience.
User experience разный даже при одинаковом размере ASIO-буферов — это же распределённая система :)

А вот если хочется именно best user experience у каждого участника, то, как и всегда при работе с ASIO и звуком вообще, надо каждому ставить его минимальный (работающий без щелчков) размер буфера.
Но тогда не было ни ASIO
Кстати вполне себе было :)
Вот появился новый сервис на эту тему: jammr.net/
Правда сам еще не проверял… выглядит особо интересно что работает и на Linux ( чего не хватает в ejamming)
Sign up to leave a comment.

Articles