147 lines
6.3 KiB
Markdown
147 lines
6.3 KiB
Markdown
## SFML Streaming
|
||
|
||
Небольшой проект на C++ и SFML для локального воспроизведения и UDP-трансляции аудио.
|
||
|
||
В проекте есть две исполняемые программы:
|
||
- `producer` - источник аудио. Может читать аудиофайл или захватывать микрофон, приводить поток к внутреннему формату и отправлять его по UDP одному или нескольким получателям.
|
||
- `consumer` - приемник аудио. Слушает UDP-порт, принимает аудиочанки и сразу воспроизводит их локально.
|
||
|
||
Как это работает:
|
||
- `producer` получает звук из файла или с микрофона.
|
||
- входной поток приводится к формату `PCM 16-bit mono 44100 Hz`.
|
||
- дальше поток режется на чанки и каждый чанк отправляется отдельной UDP-датаграммой.
|
||
- `consumer` принимает датаграммы, интерпретирует их как массив `int16`-сэмплов и подает в `sf::SoundStream`.
|
||
|
||
### Сборка
|
||
|
||
Требования:
|
||
- CMake
|
||
- компилятор с поддержкой C++17
|
||
- SFML с модулями `system`, `audio`, `network`
|
||
|
||
Установка зависимостей (debian/ubuntu)
|
||
```bash
|
||
sudo apt install g++ cmake make libsfml-dev
|
||
```
|
||
|
||
Сборка:
|
||
|
||
```bash
|
||
cmake -S . -B build
|
||
cmake --build build
|
||
```
|
||
|
||
### Запуск
|
||
|
||
Приемник:
|
||
|
||
```bash
|
||
./build/consumer 5000
|
||
```
|
||
|
||
Передача файла:
|
||
|
||
```bash
|
||
./build/producer file input.wav 5000 127.0.0.1
|
||
```
|
||
|
||
Передача микрофона:
|
||
|
||
```bash
|
||
./build/producer mic 5000 127.0.0.1
|
||
```
|
||
|
||
Флаг `--no-local` отключает локальное воспроизведение на стороне `producer`.
|
||
|
||
## Контракт формата трансляции
|
||
|
||
Этот раздел описывает точный формат UDP-потока, который ожидает `consumer` и отправляет `producer`.
|
||
|
||
### Транспорт
|
||
|
||
- Протокол: UDP
|
||
- Одна UDP-датаграмма = один аудиочанк
|
||
- Соединение не устанавливается, подтверждения доставки нет
|
||
- Пакеты могут теряться, дублироваться или приходить не по порядку: приложение это не исправляет
|
||
|
||
### Полезная нагрузка UDP-пакета
|
||
|
||
- Только сырые PCM-данные
|
||
- Заголовка пакета нет
|
||
- Метаданных нет
|
||
- Идентификатора потока нет
|
||
- Порядкового номера пакета нет
|
||
- Временной метки нет
|
||
- Контрольной суммы на уровне приложения нет
|
||
|
||
Иными словами, payload каждого UDP-пакета - это просто последовательность байт, которую приемник читает как массив 16-битных signed little-endian сэмплов.
|
||
|
||
### Аудиоформат
|
||
|
||
- Кодирование: PCM
|
||
- Тип сэмпла: signed 16-bit integer
|
||
- Порядок байт: little-endian
|
||
- Каналы: `1` (`mono`)
|
||
- Частота дискретизации: `44100 Hz`
|
||
|
||
Эквивалентная краткая запись формата:
|
||
|
||
```text
|
||
PCM s16le mono 44100 Hz
|
||
```
|
||
|
||
### Интерпретация payload
|
||
|
||
Каждые 2 байта образуют один сэмпл `int16`:
|
||
|
||
```text
|
||
byte0 byte1 -> sample0
|
||
byte2 byte3 -> sample1
|
||
...
|
||
```
|
||
|
||
Пример одного сэмпла:
|
||
- байты `0x34 0x12` будут интерпретированы как значение `0x1234`
|
||
|
||
### Размер пакета
|
||
|
||
По текущей реализации приемник читает не более `4096` байт за одну UDP-датаграмму.
|
||
|
||
Практические требования:
|
||
- размер payload должен быть не больше `4096` байт
|
||
- размер payload должен быть кратен `2`, потому что один PCM-сэмпл занимает 2 байта
|
||
- безопаснее отправлять заметно меньше MTU сети, чтобы снизить риск фрагментации UDP-пакетов
|
||
|
||
`producer` в текущей реализации обычно отправляет до `2048` сэмплов за раз, то есть до `4096` байт payload.
|
||
|
||
### Что приемник не поддерживает
|
||
|
||
`consumer` не умеет автоматически разбирать или декодировать:
|
||
- WAV, RIFF или любой другой контейнер
|
||
- MP3, AAC, Opus, Vorbis, FLAC и другие сжатые форматы
|
||
- `float32` PCM
|
||
- `stereo` или многоканальный поток
|
||
- частоты дискретизации, отличные от `44100 Hz`
|
||
- пользовательские заголовки перед PCM-данными
|
||
|
||
Если передатчик отправляет любой из этих форматов, звук на приемнике будет искажен или не будет воспроизводиться корректно.
|
||
|
||
### Контракт для внешнего передатчика
|
||
|
||
Если поток отправляется не из `producer`, а из другого приложения, например из Dart, внешний передатчик должен:
|
||
- открыть UDP-сокет
|
||
- отправлять датаграммы на порт, который слушает `consumer`
|
||
- помещать в payload только raw `PCM s16le mono 44100 Hz`
|
||
- не добавлять заголовок перед PCM-данными
|
||
- следить, чтобы размер каждой датаграммы был не больше `4096` байт и кратен `2`
|
||
|
||
Минимально совместимый контракт можно сформулировать так:
|
||
|
||
```text
|
||
UDP packet payload = raw PCM samples
|
||
sample format = signed 16-bit little-endian
|
||
channels = 1
|
||
sample rate = 44100 Hz
|
||
max payload = 4096 bytes
|
||
```
|