Войти
ПроектыФорумОцените

s3 (Simple Sound System) - yet another sound library

#0
16:42, 13 мая 2010

http://code.google.com/p/s-3/downloads (новые версии будут доступны только через hg. если у кого не стоит TortoiseHG, последнюю версию можно скачать здесь)

Итак, еще одна звуковая библиотека. Зачем? Существует же огромное кол-во звуковых библиотек, под все мыслимые и немыслимые нужды? Вот именно. Под все. А нужно под вполне конкретные. А все эти большие и сложные библиотеки: семплы, фрагменты, каналы, группы, приоритеты, стримы, обработчики-переобработчики, эффекты-переэффекты. 90% возможностей этих библиотек как правило никогда и не потребуется, даже позиционирование звука и то не припомню чтобы использовалось в нашем предыдущем проекте. И в то же время, самые необходимые вещи, вроде проигрывания звука в памяти из shared buffer-а с подсчетом ссылок, приходится реализовывать через одно место.
И примеров как нужно делать, примеров грамотного, эффективного использования, а не тупо "проиграть звук из файла с echo-эффектом" - тоже хрен найдешь.
В общем, я решил, раз все равно надо разбираться в том "как надо делать", то лучше делать это с самого низкого уровня (DirectSound/XAudio2), который впрочем не такой уж и "низкий". (В текущей реализации либы я жестко завязался на DirectSound, и XAudio2-версии скорее всего не будет.)
Итого. Основная задача библиотеки - не сферическое проигрывание звука в вакууме, а простое, но в то же время достаточно эффективное (по крайней мере по меркам PC :) ) объектно-ориентированное (ох уж эти Си интерфейсы) решение конкретных задач, необходимых обычным интерактивным развлекательным мультимедиа-приложениям (по-русски - играм :) ).
Основные возможности библиотеки:
1. Воспроизведение сжатого звука, целиком находящегося в памяти. Вообще считаю целесообразным хранить все звуки только в сжатом виде (и на диске, и в памяти), и распаковывать их по мере проигрывания, а в качестве основного формата использовать FLAC, как имеющего (AFAIK) самую высокую скорость распаковки. Одно ядро Core Quad Q6600 может распаковывать одновременно более 2000 flac-потоков. Т.о. распаковка 100 звуков занимает лишь 5% CPU, т.е. при времени кадра в 10 мс, распаковка будет занимать 0.5 мс, что считаю, не такой уж и высокой ценой за уменьшение размера всех звуков в 2-3 раза.
2. Воспроизведение больших звуковых файлов (музыки напр.) с префетчем. Наиболее часто реализуемая схема проигрывания потоковых звуков - через чтение файла в отдельном потоке - очевидно, не является оптимальной, и к тому же очень плохо оптимизируется системой (простой тест, параллельное чтение 2-х файлов (из 2-х потоков) выполняется в 4 раза дольше последовательного чтения этих же файлов из одного потока, параллельное чтение 3-х файлов - уже в 10 раз дольше). Гораздо лучше все операции чтения проводить через единую систему асинхронной загрузки ресурсов, которая так или иначе должна присутствовать в любом современном движке.
3. Возможность воспроизведения RAW-потока (т.е. звуковых семплов, уже распакованных каким-то другим способом и которые не имеет смысла загонять в контейнер wav/ogg). Например, для проигрывания звукового потока из ogg-файла, содержащего также видео theora.

Есть еще концепция, которую предлагают использовать Valve: хранить 0,5 сек. всех звуков в игре и при начале проигрывания звука догружать остальное. Идея хорошая, но:
1. Нет гарантии, что остальная часть звука успеет подгрузиться за это время, т.к. в очереди асинхронной загрузки может и так стоять много файлов.
2. Все звуки в игре обычно достаточно маленькие, и их и так можно хранить в памяти для тех объектов, к которым они относятся (по крайней мере на PC), т.е. все что связано с игровым объектом (модель, текстуры, анимации, звуки) можно хранить в памяти целиком пока объект стоит на мире (или пока мы его видим). Минута звука во flac (моно, 22КГц) занимает около 1Мб. Все звуки больше полминуты (а таких очень мало по сравнению с остальными) можно хранить в vorbis (вообще все звуки в vorbis лучше не хранить из-за медленной распаковки по сравнению с flac - примерно в 3-4 раза).
Поэтому (и не только) данная концепция в s3 не поддерживается.

Немного деталей реализации.
Основная идея: постоянно иметь N буферов-слотов и все звуки проигрывать через них, т.о. не создавая IDirectSoundBuffer при каждом проигрывании звука. Это необходимо прежде всего для реализации потокового декодирования flac, т.к. декодирование звука в 77 сек занимает 33 мс, и лучше декодировать его не весь сразу, а по мере проигрывания. А также, в таком случае, разделение на потоковые и одиночные звуки не имеет смысла - все звуки можно считать потоковыми.
Буферам в слотах иногда требуется менять частоту, для этого их лучше пересоздавать (т.к. другие варианты - ресемплить программно с помощью libresample, либо вызывать SetFrequency, не подходят, к тому же, обычно все звуки в игре имеют одну частоту и пересоздания буферов происходить не будет).

Теперь перейду к практическому использованию библиотеки (в исходниках есть примеры 3-х классов источников звука, но они слишком простые и искусственные - в реальных проектах так конечно никто делать не будет).
Итак, есть игровые объекты, к которым могут быть прицеплены источники звука. Звук необходимо проигрывать только если он находится в пределах слышимости.
Проверку дальности от источника звука до камеры нужно осуществлять до начала проигрывания звука (останавливать звук при выходе за его радиус необязательно). После чего создается и запускается сам источник звука с вызовом autoDelete(). Это, если звук одиночный (шаги, звуки выстрелов и т.п., практически все звуки в игре).
Циклически играющие звуки нужно останавливать при выходе за больший радиус, а запускать при входе в меньший радиус (гистерезис), причем play нужно вызывать с временем time > 0 (напр. 1 секунда). Мгновенно включать звук нельзя - это фундаментальная проблема, т.к. проигрывание звука с начала приведет к остановке играющего звука (т.к. на источник звука выделен только один декодер и невозможно плавно останавливать звук и одновременно запускать его же с начала - можно только продолжать играть его, плавно увеличивая громкость до максимума), фактически, именно по этой причине, повторное воспроизведение генерирует ошибку (но если time > 0, то все нормально).
Таково мое видение "как надо делать".

Пример практического использования.

class AutoSoundSource//кратковременно-играющий источник звука, автоматически удаляющийся по окончании воспроизведения
{
  struct SoundSource : s3::MSource
  {
    SharedFileBuffer soundBuf;
    SoundSource(s3::SoundGroup group) : MSource(group) {}
  };

public:
  SharedResourcePtr<SharedFile> file;

  float radius;//радиус источника звука

  void play()
  {
    if (!file->isLoaded) return;//если файл еще не успел загрузиться - игнорируем команду проигрывания звука
    vec3 position = <...>;
    if (sqlen(::camera.position() - position) > radius*radius) return;//источник слишком далеко

    SoundSource *ss = new SoundSource(s3::SG_3D);
    ss->volume = <расчет_громкости>;
    memcpy(ss->position, &position, sizeof(float[3]));
    ss->soundBuf = file->buffer;
    ss->init(ss->soundBuf.data(), ss->soundBuf.size());
    ss->play();
    ss->autoDelete();//самый простой способ, когда не требуется изменение параметров звука во время его проигрывания (если требуется,
      //нужно использовать weak pointer-ы или др. способы слежения за временем жизни объекта)
  }
};

class LoopedSoundSource : public AutoSoundSource//зацикленный постоянно-играющий звук
{
  s3::MSource soundSource;
  void onLoadFile() {soundSource.init(file->buffer.data(), file->buffer.size());}

  bool playing, needPlay;

public:
  LoopedSoundSource() : playing(false), needPlay(false)
  {
    file.onLoadNotifier = MakeDelegate(this, &LoopedSoundSource::onLoadFile);
    soundSource.volume = <расчет_громкости>;
    file.load(fileName);//старт асинхронной загрузки файла со звуковыми данными
  }

  void play()
  {
    needPlay = true;
    update();
  }

  void stop(float time = 1.f)
  {
    MSource::stop(time);
    playing = needPlay = false;
  }

  void update(const vec3 &position)
  {
    if (needPlay)
    {
      memcpy(soundSource.position, &position, sizeof(float[3]));

      float sqdist = sqlen(::camera.position() - position);
      if (playing)
      {
        if (sqdist > square(radius*1.1f/*гистерезис*/)) MSource::stop(1.f), playing = false;//источник вышел за пределы слышимости
      }
      else
        if (sqdist < square(radius)) MSource::play(true, 1.f), playing = true;//источник вошел в радиус слышимости - запускаем его
    }
  }
};


#1
17:06, 13 мая 2010

Почему не OpenAL,а DirectSound?
ЗЫ Много букв не осилил,но я так понял стримминга нету?

#2
17:40, 13 мая 2010

Igor'
>Почему не OpenAL,а DirectSound?
По той же причине я отказался от XAudio2 - требование установки  сторонних dll. Нужно чтобы финальный exe работал как есть, чтобы можно было перенести игру на флешке напр. и спокойно запустить на др. компе ничего перед этим не устанавливая (напр. WoW так переносится). Можно было бы в ланчере проверять установлены ли необходимые библиотеки (OpenAL или d3dx для XAudio2), хранить дистрибутивы вместе с игрой и устанавливать перед запуском игры, но зачем, если можно без этого?
>но я так понял стримминга нету?
У меня механизм проигрывания звука только один - потоковый. Очевидно, стриминг сделать не проблема (в example.cpp есть пример). Но я считаю, для музыки лучше делать prefetch.

#3
17:54, 13 мая 2010
респект за codestyle
#4
18:29, 13 мая 2010

tav
> Нужно чтобы финальный exe работал как есть, чтобы можно было перенести игру на
> флешке напр. и спокойно запустить на др. компе ничего перед этим не
> устанавливая
ну dll OpenAL можно и в корне проги хранить, да и весит всего 50кб, зато OAL задел на кросс платформу.
На Delphi прикрутить можно будет? я так понял классы, значит нет?
Пойду качать, пока не смотрел..., проект вроде годный, удачи!

#5
18:50, 13 мая 2010

tav
Странно слышать о использовании DirectSound вместо OpenAL для переносимости. В конце концов статическую линковку ещё не отменили.

#6
19:07, 13 мая 2010

master-sheff
> ну dll OpenAL можно и в корне проги хранить, да и весит всего 50кб
50 вроде весила самая первая версия dll-ки. которая под win98, и не работала под xp. помню, как намучился искать ошибку инициализации контекста :-)
непомню как я догадался обновить dll-ку - с новой (уже за 150кб) под xp все заработало. но та версия dll-ки не работает под vista/7
В последней версии openal32.dll занимает 106кб, но нужна еще wrap_oal.dll. Но класть dll-ку рядом с игрой, я считаю, не выход - уж больно намучился я с таким подходом в свое время :)
Ну а вообще, концепция циклических буферов DirectSound-а слишком хорошо мне подошла, и использование queuing buffers значительно понизит эффективность проигрывания звука.
> На Delphi прикрутить можно будет? я так понял классы, значит нет?
нет :)
>проект вроде годный, удачи!
спасибо, но минималистичность либы думаю не всем понравится. Напр. там нет (и не планируется) позиционирования звука (установка текущей позиции воспроизведения).

Кирюшык
> Странно слышать о использовании DirectSound вместо OpenAL для переносимости. В
> конце концов статическую линковку ещё не отменили.
А OpenAL можно прилинковать статически?

#7
8:11, 14 мая 2010

Пачка замечаний (недавно как раз имплементил аудио для ПС3, воспоминания свежи :)

Вот это "проигрывания звука в памяти из shared buffer-а с подсчетом ссылок, приходится реализовывать через одно место" странно -- есть Buffer'ы с сэмплами (PCM или что-то сложнее), есть Source'ы (которым назначают buffer и делают play/stop и т.п.), Source'у для проигрывания нужен свободный hardware-voice. Так построено практически всё аудио-железо, так построены почти все аудио-либы.

Самый быстрый в распаковке таки ADPCM, даже побыстрее memcpy местами :)
С компрессированным звуком главная проблема -- задержка начала проигрывания, т.е. от момента когда код сказал play и до момента когда первые байтики уходят в железку (и мы слышим); если это звук типа удара/выстрела, то задержки такие -- очень не здорово.

OpenAL можно при определённых усилиях залинковать статически и не таскать за собой ДЛЛки.

Не использовать позиционирования (spatialization) ИМХО попросту глупо -- уже лет пять самые дешовые пейсишный встроенные карточки за 1.5 бакса замечательно играют 3Д-звук (то что укладывается в модель OpenAL 1.1)

#8
14:28, 14 мая 2010

Ghost Dragon
Для начала я немного обрисую ситуацию, достаточно типовую для любого игрового проекта. Есть игровой объект, напр. пистолет. За ним закреплены некоторые графические и другие ресурсы, в том числе звуки (выстрелы, перезарядка и т.д.). Если на мир поставлен хотя бы один инстанс объекта, который ссылается на этот пистолет, то все эти ресурсы нужны или могут понадобиться в любой момент. Подгружать звук выстрела в тот момент когда его уже нужно воспроизводить - плохая идея, т.к. будет большая задержка, значит когда есть пистолет, есть и все звуки которые с ним ассоциированы. Хранить звуки, как и все другие ресурсы, целесообразно в буферах с рефкаунтом (т.е. с шарингом, или с подсчетом ссылок - одно и то же), чтобы когда 10 разных пистолетов использовали один и тот же звуковой файл - он в памяти хранился в единственном количестве. Теперь простой пример - пистолет выстрелил и тут же был удален как игровой объект (игрок сменил оружие или еще по какой причине - неважно). И так оказалось, что рефкаунт этого буфера перед удалением был равен 1, т.о. после удаления пистолета буфер звука удалится (а звук уже начал играться и обрывать его на середине нельзя!), если только при начале проигрывания вручную не поднимать ему рефкаунт, а по окончании опускать (что порой очень непросто делается, особенно когда звуковая библиотека многопоточная, и callback окончания проигрывания вызывает откуда угодно, а опускание рефкаунта с удалением объекта в игровом коде можно делать только из одного потока).

> странно -- есть Buffer'ы с сэмплами (PCM или что-то сложнее), есть Source'ы (которым назначают buffer и делают play/stop и т.п.)
Да, есть. Но что с ними делать? Если полностью самому менеджить буферы (шарить их между одинаковыми файлами, удалять когда больше нет объектов, на них ссылающихся и т.д.), то как разрешать ситуации, когда этот буфер уже не нужен (мне), а Source'ы его использующие все еще играют?
В OpenAL это запрещенная операция:
A buffer which is attached to a source can not be deleted. (5.2.2. Releasing Buffer Names)
Ага, значит сначала нужно деаттачить буфер. Но не все так здорово:
The alSourcei(sName, AL_BUFFER, AL_NONE) call still causes an AL_INVALID_OPERATION for any source in the AL_PLAYING or AL_PAUSED state, consequently it cannot be used to mute or stop a source.
Звуковые движки должны быть умнее чем OpenAL в этом плане, ведь правда? Посмотрим.
SQUALL. После вызова SQUALL_Sample_Play сразу вызываю SQUALL_Sample_Unload - нет звука.
Так, теперь fmod. Не поленился, скачал последнюю версию (4.31.02). В документации ничего по этому поводу не нашел, решил проверить на практике. Открыл самый простой пример (playsound). После строчки result = system->playSound(FMOD_CHANNEL_FREE, sound1, false, &channel); добавил sound1->release();. Все. Звук не играет, что и требовалось доказать. А это, именно то, как хочется делать и как удобно делать. Почему при создании канала не поднимается ref звуку, из которого берутся данные? Останавливать проигрывание нужно когда я удаляю канал, а не звук! Не спорю, возможно в FMOD есть средства которые позволяют это делать, в крайнем случае можно вручную в главном цикле (или через нотификатор) отслеживать, играется ли канал, звук которого уже не нужен, и удалять звук когда он уже не играется и т.д. и т.п. Но почему оно сразу не работает так, как это удобно?
В irrKlang я сразу нашел эту фишку - когда звук не нужен можно вызвать метод drop (аналог моего autoDelete, только более умный, с подсчетом ссылок - у меня все проще). Но по-прежнему остается открытым вопрос - как играть звук из своего буфера? Функция создания звука из памяти (addSoundSourceFromMemory) позволяет указать нужно ли копировать буфер или нет. Но мне не подходит ни то, и ни другое! Если я укажу свой буфер, то мне придется отслеживать время жизни звука, а точнее всех каналов играющих этот звук, чтобы не удалить свой буфер раньше времени, т.к. данные моего буфера может использовать звуковая библиотека. Указать копировать - тоже не выход, т.к. теряется контроль над временем жизни буфера. Мне нужно чтобы буфер жил пока жив мой объект, который в любой момент может издать звук, с чего ради я должен доверять управление временем жизни и шаринг звуковых буферов библиотеке, когда это у меня уже есть и когда мне удобнее это делать? И так вот, очень простые вещи приходится делать очень непросто.
Т.е., я хочу просто удалить буфер, а все играющие его звуки прерывать не хочу. Более того, буфер - это мой класс, он умеет шаринг (т.е. он с рефкаунтом), он же используется в системе файлового кэширования, его я передаю в нотификатор окончания асинхронной загрузки и т.д. Я не хочу, чтобы звуковая библиотека куда-то копировала мои данные, делала лишние аллокации и как-то сама управляла этими данными, нафик вообще все это - все же тривиально решается рефкаунтом, ну правда же!
> так построены почти все аудио-либы.
Но это же не удобно! Поэтому то я и отошел от этой схемы с buffer-ами и source-ами (семплами и звуками), т.к. она более сложная и не дает необходимой гибкости. Ну т.е. вместо кучи сущностей - семплы, буферы, фрагменты, каналы - я сделал только одну - источник звука. А где брать данные для него полностью решает пользовательский код.

>Самый быстрый в распаковке таки ADPCM, даже побыстрее memcpy местами :)
Не нашел в инете цифр скорости распаковки, потом надо будет попробовать. Но верится с трудом. :-)
И это формат с потерей качества, поэтому многие до сих пор используют wav. А среди форматов без потери качества flac - один из лучших. И плюс у ADPCM сжатие до 4:1, а flac может и в 5 и в 10 раз сжать, причем без потерь (у нас было несколько довольно больших ambient-звуков, которые очень хорошо сжались flac-ом с 3Мб до 300Кб).

>С компрессированным звуком главная проблема -- задержка начала проигрывания
Для минимизации задержки я специально сделал, чтобы начало проигрывания выполнялось сразу в том же потоке. Сама задержка составляет около 0.25 мс (примерно столько времени нужно для распаковки и заполнения полсекундного буфера звука, и то я думаю уменьшить размер буфера в 2-3 раза, что пропорционально уменьшит и задержку).

> OpenAL можно при определённых усилиях залинковать статически и не таскать за собой ДЛЛки.
Хм. А через что он будет работать в этом случае? Через DirectSound? А при наличии OpenAL-драйвера? И насколько совместимо такое решение между разными версиями Windows (XP/Vista/7)?

>Не использовать позиционирования (spatialization) ИМХО попросту глупо -- уже лет пять самые дешовые пейсишный встроенные карточки за 1.5 бакса замечательно играют 3Д-звук (то что укладывается в модель OpenAL 1.1)
Да не, я про другое говорил :) Под позиционированием звука я понимаю установку позиции воспроизведения (SetPlayPosition), т.е. перемотку, а не положение источника звука в пространстве.

#9
15:00, 14 мая 2010

tav
audio-buffer -- ресурс, т.е. менеджится движком; задумываться про то как он грузиться,удаляется,стримится и т.п. -- не дело игрового кода ни разу.

>> так построены почти все аудио-либы.
>Но это же не удобно!
такова жизнь (точнее железо)  :)
и с этим вполне нормально живётся, точнее high-level функциональность нормально строитсяповерх этого low-level
а позволять пользовательскому коду напрямую работать с буферами/воисами -- зло (просто по опыту писания звука в разных играх)

>> OpenAL можно при определённых усилиях залинковать статически и не таскать за собой ДЛЛки.
>Хм. А через что он будет работать в этом случае? Через DirectSound?
> А при наличии OpenAL-драйвера? И насколько совместимо такое решение между
> разными версиями Windows (XP/Vista/7)?
всегда будет юзаться дсаунд, на практике опенАЛ-дривер нормально имплемитится только на проф.карточках которых гм статистически немного :)
на всех виндах работает -- вон мой прототип про мехов так и работает

#10
16:26, 14 мая 2010

Ghost Dragon
> audio-buffer -- ресурс, т.е. менеджится движком; задумываться про то как он
> грузиться,удаляется,стримится и т.п. -- не дело игрового кода ни разу.
Ну я и не говорю, что напрямую из игрового кода надо менеджить audio-buffer-ами. Я говорю, что между игровыми объектами и audio-buffer-ами должна быть некая связь (невидимая из игрового кода), и что время жизни audio-buffer-а должно контролироваться и подчиняться времени жизни игровых объектов, а не каким-то там умным правилам звуковых движков. (Как вариант, audio-buffer должен удаляться когда он не нужен, а именно когда удалили последний игровой объект, ссылающийся на него (не напрямую конечно, а через объект движка, содержащий умный указатель на audio-buffer)).

Если коротко, мне нужна гарантия, что при наличии игрового объекта, я могу мгновенно проиграть любой звук, который к нему относится, без подгрузки с диска. А также, что при смерти игрового объекта недоигравший звук никогда не будет обрываться. Простого способа сделать такое в других звуковых либах, я, к сожалению, не знаю.

#11
21:17, 14 мая 2010

tav
> между игровыми объектами и audio-buffer-ами должна быть некая связь (невидимая из игрового кода),
> и что время жизни audio-buffer-а должно контролироваться и подчиняться времени жизни игровых объектов
Да вот в том и дело что игровой код не должен знать про audio-source и тем более про audio-buffer. Audio-buffer это вообще ресурс -- тупо байтики с уникальным именем. Менеджится он (должен бы по-хорошему) точно так же как и все остальные ресурсы, к звуковой библиотеке это никакого отношения не имеет, совсем. Точно так же как, скажем, игровой код не занимается подгрузкой/стримингом текстур.

Вопросы "гарантии наличия" и "обрыва звука при смерти игрообъекта" тоже относятся к high-level коду игродвижка. Звуковая либа должна делать свои вещи -- играть звук, миксить / процессить / эффекты накладывать и т.п.

Прошло более 10 месяцев
#12
17:28, 20 мар. 2011

Ghost Dragon
> Вопросы "гарантии наличия" и "обрыва звука при смерти игрообъекта" тоже
> относятся к high-level коду игродвижка.
Да простят меня некропостненавистники (уж скоро год этому треду), но ведь это означает, что не "бери либу и пользуй", а "бери либу, напиши к ней обертку и только тогда пользуй". Понятно, что первый вариант предполагает некую заточенность кода под либу, но не типовая ли это задча - играть звук после смерти объекта? Сдается мне, что типовая. Тогда почему всякая уважающая себя звуковая либа обязывает очередного программиста писать этот велосипед?

PS. Либа взлетела. Очень удобно ее пользовать.

ПроектыФорумОцените

Тема в архиве.