Звук Voxwing · от записи до боя
Как звук попадает в игру и как с ним работать без кода: найти запись, нарезать сэмпл, подключить, свести, проверить. Всё — на двух настоящих заменах: мотор Cessna → Spitfire XIV и пулемёт G36 → Warfare Library.
- 35 сэмплов · 4.5 МБ из 20
- 21 звук
- микс −15.7 LUFS
- PR #390, #391
Звук на странице включается кнопками ▶. Нужен браузер с Opus в .ogg: Chrome, Firefox, Edge; Safari — с macOS 14 / iOS 17.
1. Путь звука
Каждый звук в игре проходит одну и ту же цепочку. Нажмите на блок — что он делает и где живёт.
Коротко: запись режется по рецепту в сэмпл. Реестр говорит, какой звук играет какие сэмплы и как они следуют за числами игры. Игра сообщает событие («выстрел», «обороты 2100»), ядро ставит голос в пространство и отправляет в канал → шину → мастер.
Где что лежит
| Что | Файл | В git | Кто правит |
|---|---|---|---|
| Исходные записи (банки) | assets-src/audio/ | нет | вы: кладёте или делаете ссылку |
| Каталог записей: поиск, громкость, атаки | assets-src/catalog.db | нет | npm run sound -- catalog … |
| Готовые сэмплы | public/assets/audio/*.ogg | да | только нарезка по рецепту |
| Реестр: сэмплы и рецепты, звуки, слои, кривые | src/data/sounds.json | да | команды, окна «Сэмплы» и «Кривые» |
| Микс: каналы, шины, уровни, trims | src/data/mix.json | да | микшер в игре, Ctrl+S |
| Правила «что как звучит» (код) | src/view/sound/ | да | программист |
2. Словарь
Одно слово — одно значение. Дальше на странице слова только в этих смыслах.
- Запись
source - Исходный файл из банка: WAV или FLAC, часто стерео, 96 кГц. Лежит только у вас.
- Сэмпл
sample - Набор коротких файлов
id-01.ogg,id-02.ogg… — варианты одного события. Mono, 48 кГц, Opus. - Вариант
- Один файл сэмпла. Выстрелы чередуются по вариантам без повтора подряд — очередь не звучит как «пулемётная дробь» из одного файла.
- Рецепт
recipe - Несколько чисел в реестре: откуда и как резать. Одни числа — побайтно один файл.
- Звук
sound - То, что игра просит сыграть:
engine,mg,hit. Играет один или несколько сэмплов. - Роль
- Свой (
own) или чужой (enemy): одно событие звучит по-разному. Свой пулемёт — в голове, чужой — из точки в небе. - One-shot · луп · поток
- One-shot — голос на событие (выстрел). Луп играет всё время и следует за числами (мотор). Поток — длинный файл (музыка).
- Слой
layer - Один сэмпл внутри звука со своими кривыми. Мотор — 4 слоя: холостой, low, mid, high.
- Терм, кривая
- Правило «число игры → множитель»: обороты → громкость слоя, обороты → высота. Громкость слоя — произведение термов.
- Канал · шина · мастер
- Канал — полоса пульта для группы звуков (
guns,own). Каналы сходятся в шинуsfx, она и музыка — в мастер. - Уровень
- Громкость одного звука в его канале (строка маршрутизации, ⇧F11).
- Trim
- Поправка громкости одного сэмпла, дБ. Выравнивает новую запись под старую, не трогая файл.
- Атака
- Начало удара: окно 5 мс громче порога. По атакам режутся выстрелы.
- LUFS
- Громкость «на слух», а не по пику. Два звука с одинаковым LUFS кажутся одинаково громкими.
3. Кейс: мотор Spitfire XIV вместо Cessna
Шаг 1. Найти запись
Банки — тысячи файлов на десятки гигабайт. Глазами по папкам не ищем: каталог знает слова из имён, папок и метаданных каждого файла.
npm run sound -- catalog scan # один раз, ~3 с: индекс 2672 файлов npm run sound -- catalog search spitfire # 7 записей npm run sound -- catalog measure spitfire # громкость и атаки только у найденных
| Запись | длина | LUFS | Годится? |
|---|---|---|---|
| Cessna, interior, in flight (Sound Ideas) | 74 с | — | было: гражданский одномоторный |
| Spitfire Mk XIV, onboard, startup, steady rpms, cockpit (Pole Position) | 470 с | −21.7 | да: кабина, ступени оборотов от холостого до полных |
| Spitfire Mk IX, onboard, idle blips | 69 с | −29.8 | только холостой и перегазовки |
| Spitfire Mk XIX overhead, Mk XIV passby | 83–89 с | — | нет: пролёт с доплером внутри — игра считает доплер сама |
| B-25 ext, B-17 ext | 159–165 с | −20 … −27 | кандидаты на чужой мотор снаружи (позже) |
Шаг 2. Где в записи ровные обороты
Мотор в записи гоняют ступенями: холостой, прогазовка, снова холостой. Для лупа нужен отрезок, где обороты стоят. График — громкость записи по 5 с; полосы — плато, из которых нарезаны лупы.
Как узнать обороты по звуку. У V12 каждый ряд из 6 цилиндров даёт 3 вспышки за оборот вала.
Микрофон в кабине ближе к выхлопу одного ряда, поэтому основной тон f0 = обороты / 20.
В спектре видно гармоники f0, 2·f0, 3·f0 и боковые полосы вокруг каждой — на половину частоты вала.
Две независимые меры сходятся — значит, обороты верные.
Луп low: гармоники 62.0 · 124.3 · 186.3 Гц, боковые ±10.4 Гц. f0 = 62.07 Гц → 62.07 × 20 = 1241 об/мин; вал 20.7 Гц, половина — 10.35 Гц ✓.
Шаг 3. Нарезать лупы
Черновик рецепта пишет команда, плато выбираем сами — в рецепте два числа:
npm run sound -- add "sonniss/…/Spitfire_Mk_XIV_t1_onbrd_…_DPA4061.R.wav" --as engine-low --loop --length 6 --force
"engine-low": {
"recipe": {
"source": "sonniss/…/Spitfire_Mk_XIV_…R.wav",
"channels": 1,
"loop": { "start": 324.5, "end": 330.5,
"crossfade": 0.5 },
"peak": -1,
"kbps": 64
}
}Луп — отрезок 6 с. Полсекунды после end вливаются в начало равномощным кроссфейдом: файл играет по кругу без шва и щелчка.
| Сэмпл | Плато в записи | луп, с | σ уровня | обороты |
|---|---|---|---|---|
engine-idle | быстрый холостой после запуска | 257–263 | 0.62 дБ | 935 |
engine-low | малая прогазовка — самая ровная | 324.5–330.5 | 0.32 дБ | 1241 |
engine-mid | средняя прогазовка | 182–188 | 0.61 дБ | 1944 |
engine-high | полная, на тормозах | 409.5–415.5 | 0.52 дБ | 2152 |
σ — разброс уровня по блокам 100 мс внутри лупа. Чем меньше, тем меньше «качается» луп по кругу.
Шаг 4. Кривые: газ → обороты → слои
Игра сообщает газ и скорость; адаптер считает обороты и нагрузку. Реестр решает, сколько звучит каждый слой и с какой
высотой: высота = обороты / recorded. Двигайте газ — слушайте, как слои передают друг другу звук.
Газ выше 1 — форсаж (WEP). Нагрузка растёт с газом и падает со скоростью: полный газ на малой скорости — тяжёлый
звук high, в пикировании — легче, mid. Пунктир — потолок высоты: выше ~2800 об/мин тон не растёт. ⚑ — высота слоя упёрлась в свой clamp.
Как это записано в реестре — слой engine-low:
{ "sample": "engine-low",
"gain": [ { "by": "alive", "from": 0, "to": 1 },
{ "code": "engineLevel" },
{ "by": "rpm", "segments": [ { "from": 1150, "to": 1400, "shape": "equalPowerIn" },
{ "from": 1400, "to": 1950, "shape": "equalPowerOut" } ] } ],
"rate": { "follow": "rpm", "recorded": 1241, "clamp": [0.75, 1.58] } }Пары equalPowerIn/equalPowerOut (синус и косинус) держат сумму мощностей = 1: на переходе громкость не проседает.
Все слои следуют за одними оборотами, поэтому в зоне кроссфейда звучат на одной высоте.
Шаг 5. Громкость: trim вместо перекодирования
Новые лупы нормированы по пику (−1 dBFS), а слышим мы LUFS. Чтобы мотор в бою не стал громче или тише, разница уходит в trim сэмпла — в mix.json, файл не трогаем.
| Луп | было, LUFS | стало, LUFS | trim |
|---|---|---|---|
| idle | −12.6 | −18.6 | +6.0 дБ |
| low | −13.8 | −13.9 | +0.1 дБ |
| mid | −14.0 | −16.7 | +2.7 дБ |
| high | −11.9 | −14.7 | +2.8 дБ |
Шаг 6. Проверить
npm run sound -- verify engine-idle engine-low engine-mid engine-high # same as in place ×4 npm run typecheck && npm test # 821 тест
verify заново режет во временную папку и сравнивает с файлами в репозитории: длительность точно, пик ±0.1 дБ.
Тест мотора проверяет, что слои, звучащие вместе, никогда не расходятся по высоте.
4. Кейс: пулемёт Warfare Library вместо G36
Шаг 1. Выбрать на слух — в темпе игры
Отдельный выстрел обманывает: важно, как звучит очередь в темпе игры — 13 выстрелов в секунду. Для каждого кандидата собрана очередь из 24 выстрелов по 1/13 с, громкость выровнена.
Проверка на клиппинг. Запись, упёршаяся в 0 dBFS, срезает вершины волны — на слух «рваный» щелчок. Перед выбором меряем: сколько сэмплов на максимуме и flat factor (длина плоских участков).
ffmpeg -i запись.wav -af astats=measure_overall=Peak_count+Flat_factor:measure_perchannel=none -f null -
| Запись | сэмплов на пике | flat factor | Вывод |
|---|---|---|---|
| Warfare, MG close, large field | 130 281 | 37.7 | клип; оставили сознательно — плотный щелчок на месте |
| MG 42, Mountain Top | 9 476 | 46.0 | клип |
| M2 .50, склад | 66 843 | 31.9 | клип и реверб помещения |
| M1919, 5 м, холостые | 40 | 11.7 | почти чистый, 3 выстрела |
| M1919, designed | 6 | 0 | чистый |
клиппинг — решение на слух, а не запрет. Но знать о нём нужно до выбора, а не после.
Шаг 2. Нарезка по атакам
npm run sound -- onsets "sonniss/…/warfare_t2_mg_firing_close_projectile_tail_large_field_….wav" npm run sound -- add "…" --as mg-shot --slices 8 --force
add --slices 8 ищет 8 атак подряд с самыми ровными пиками и режет каждую: за 2 мс до атаки, длина — 92 % шага между атаками, 10 мс затухания в конце.
Вторая очередь записи (12.7–17.6 с): высота риски — пик атаки. Выделены 8 вариантов 13.778–14.398 с: шаг 85–95 мс, пики в пределах 0.1 дБ.
"recipe": { "source": "sonniss/…/warfare_t2_mg_firing_close_….wav", "channels": 1,
"starts": [13.778, 13.868, 13.953, 14.043, 14.133, 14.228, 14.313, 14.398],
"length": 0.082, "fadeOut": 0.01, "peak": -1, "kbps": 96 }Шаг 3. Хвост очереди
После последнего выстрела поле ещё звучит секунды: эхо и шелест уходящих пуль. Это отдельный сэмпл mg-tail:
он стартует с каждым выстрелом с отступом tail.at, а следующий выстрел его гасит. Слышен только после последнего.
Огибающая после последней атаки (17.55 с), блоки 50 мс. Хвост — 2 с от +72 мс, затухание 0.5 с в конце.
"peak": -10.84.Шаг 4. Очередь как в игре
Так играет ядро: вариант без повтора подряд, у каждого выстрела высота ±3 % и громкость ±1.5 дБ (spread), хвост после последнего.
Каждый прямоугольник — выстрел, цифра — номер варианта. trim обоих наборов — как в игре: G36 0 дБ, Warfare −3.4 дБ.
| Очередь 24 выстрела + хвост | было | стало | trim |
|---|---|---|---|
| Выстрелы | −15.5 LUFS | −12.1 LUFS | −3.4 дБ |
| Хвост | −42.1 LUFS | −28.8 LUFS | −3.4 дБ |
trim одинаковый у выстрела и хвоста — соотношение из записи сохраняется. Хвост стал заметнее на ~10 дБ: так звучит поле. Тише — trim mg-tail в окне «Сэмплы».
5. Сделай сам: замена сэмпла по шагам
Порядок, по которому прошли оба кейса. Каждая команда — из корня репозитория.
- Записи на местоПоложите банк в
assets-src/audio/или сделайте ссылку — копировать гигабайты не нужно.ln -s ~/Downloads/"Sonniss GDC 2019" assets-src/audio/sonniss-gdc-2019 npm run sound -- catalog scan
- Найти и измерить кандидатов
Слушайте в темпе игры. Проверьте клиппинг (
npm run sound -- catalog search machine gun --max 60 npm run sound -- catalog measure machine gun --max 60
astats, раздел 4). - Посмотреть атаки или платоВыстрелы —
onsets; луп — ровный по уровню отрезок.npm run sound -- onsets "путь/к/записи.wav"
- Черновик и нарезка
Или мышью: микшер → «Сэмплы» → вкладка «Исходник», маркеры на волне, Save.
npm run sound -- add "путь/к/записи.wav" --as mg-shot --slices 8 --force # выстрелы npm run sound -- add "путь/к/записи.wav" --as engine-low --loop --length 6 --force # луп
- Поправить числа и перенарезатьВ
src/data/sounds.json→samples.<id>.recipe:starts,length,loop,peak.npm run sound -- cut mg-shot
- ОписатьВ
samples.<id>:description,sources(банк и файл),cut— словами, как нарезано. - Подключить и подстроитьСэмпл в
samplesзвука; у лупа —recordedпод обороты записи. Подnpm run devигра подхватывает правку без перезагрузки: в консолиSound: registry reloaded. - СвестиГромкость старого и нового — LUFS; разницу — trim в окне «Сэмплы» (не перекодированием). Уровень звука — ⇧F11. Ctrl+S.
- Проверить и закоммитить
В коммит: файлы
npm run sound -- verify mg-shot mg-tail npm run typecheck && npm test
public/assets/audio/,sounds.json,sounds.gen.ts,mix.json.
assets-src/; коммитить assets-src/ и catalog.db;
нормализовать варианты по отдельности (громкость начнёт скакать); класть файл без записи в реестре; менять громкость перекодированием.6. Микшер в игре
Сводим прямо в бою: правка слышна сразу, Ctrl+S пишет её в src/data/mix.json.
npm run dev # затем http://localhost:5173/?duel&demo&mixer| Клавиша | Окно | Зачем |
|---|---|---|
| ⇧F9 | Пульт | стрипы каналов, шин, мастера; фейдеры, инсерты, M/S |
| ⇧F10 | Спектр · Loudness | спектр выбранного стрипа, громкость выхода против −16 LUFS |
| ⇧F11 | Маршрутизация | звук → канал, уровень звука, ▶ прослушать через канал |
| «Сэмплы» | Сэмплы и Исходник | варианты, пик, LUFS, trim; редактор рецепта на волне записи |
| «Кривые» | Кривые слоёв | термы громкости и высоты; оранжевая точка — значение в полёте |
Какую ручку крутить
Вся группа: все выстрелы, вся музыка.
Один звук в его канале: mg.enemy тише своего.
Одна запись внутри звука: новый луп под громкость старого.
Как громкость или высота следует за скоростью, оборотами, высотой.
Несохранённые правки — «● не сохранено» в тулбаре; они же в браузере, вкладку можно закрыть. «К файлу» — отбросить, A/B — сравнить с файлом на слух.
7. Шпаргалка
| Команда | Что делает |
|---|---|
npm run sound -- catalog scan | индекс записей: имена, метаданные, длина (секунды) |
npm run sound -- catalog search <слова> [--min с] [--max с] [--mono] | найти запись |
npm run sound -- catalog measure <слова> | LUFS, пик, атаки найденных |
npm run sound -- onsets <запись> [--thr дБ] [--gap с] | атаки и готовая строка starts |
npm run sound -- add <запись> --as <id> [--slices N | --loop] [--force] | черновик рецепта и нарезка |
npm run sound -- cut <id> | перенарезать по рецепту |
npm run sound -- verify <id> | --all | сверить рецепт с файлами, ничего не пишет |
npm run loudness -- --seconds=120 | громкость всего микса в демо-бою |
Mono, 48 кГц, Opus: 96 kbps — короткие, 64 — лупы и фон. Пик набора −1 dBFS. Одна длительность у всех вариантов.
4–8 на частое событие (выстрел, попадание), 1–2 на редкое (взрыв). Бюджет всех файлов — 20 МБ.
Старт за 1–2 мс до атаки; затухание 5–10 мс, у хвостов и эха 0.3–0.5 с.
Самый ровный отрезок; кроссфейд 0.04–1 с; файл без фейдов и без шва.
Подробнее — docs/guide/sound/ в репозитории: README, сэмплы, реестр, микшер, API. Почему всё так — DESIGN.md §11.31.