Звук 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дакоманды, окна «Сэмплы» и «Кривые»
Микс: каналы, шины, уровни, trimssrc/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

Было: свой мотор — кабина одномоторной Cessna. Для истребителя WW2 звучал как учебный самолёт. Стало: кабина Supermarine Spitfire Mk XIV (V12 Rolls-Royce Griffon). Issue #386, PR #390 и уточнение оборотов в #389.

Шаг 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 blips69 с−29.8только холостой и перегазовки
Spitfire Mk XIX overhead, Mk XIV passby83–89 с—нет: пролёт с доплером внутри — игра считает доплер сама
B-25 ext, B-17 ext159–165 с−20 … −27кандидаты на чужой мотор снаружи (позже)
Исходник: запуск Griffonпервые 24 с записи 470 с

Шаг 2. Где в записи ровные обороты

Мотор в записи гоняют ступенями: холостой, прогазовка, снова холостой. Для лупа нужен отрезок, где обороты стоят. График — громкость записи по 5 с; полосы — плато, из которых нарезаны лупы.

idle 935low 1241mid 1944high 2152 об/мин

Как узнать обороты по звуку. У 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 Гц ✓.

Первая оценка по одному тону дала 945 / 1260 / 1950 / 2170 — до 1.5 % мимо. Слои в зоне кроссфейда звучали бы на 20 центов врозь, «хором». Точные числа по боковым полосам — 935 / 1241 / 1944 / 2152.

Шаг 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–2630.62 дБ935
engine-lowмалая прогазовка — самая ровная324.5–330.50.32 дБ1241
engine-midсредняя прогазовка182–1880.61 дБ1944
engine-highполная, на тормозах409.5–415.50.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стало, LUFStrim
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 дБ
Было: midCessna, кабина, ~2450 об/мин
Стало: midSpitfire XIV, 1944 об/мин, trim +2.7
Было: highCessna, взлёт
Стало: highSpitfire XIV, 2152 об/мин, trim +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

Было: своя очередь — штурмовая винтовка G36 5.56, современная. Стало: пулемёт в поле из Warfare Library (Pole Position), с настоящим эхом поля. Issue #387, PR #391.

Шаг 1. Выбрать на слух — в темпе игры

Отдельный выстрел обманывает: важно, как звучит очередь в темпе игры — 13 выстрелов в секунду. Для каждого кандидата собрана очередь из 24 выстрелов по 1/13 с, громкость выровнена.

Было: G36Blastwave FX, 5.56
Warfare, вблизивыбран · шаг 0.09 с, пики ровные
M1919 .30, турельчистый, но всего 6 выстрелов
M1919, designedродной темп 18/с — паузы на 13/с
MG 42, вершинанастоящее эхо гор

Проверка на клиппинг. Запись, упёршаяся в 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 field130 28137.7клип; оставили сознательно — плотный щелчок на месте
MG 42, Mountain Top9 47646.0клип
M2 .50, склад66 84331.9клип и реверб помещения
M1919, 5 м, холостые4011.7почти чистый, 3 выстрела
M1919, designed60чистый

клиппинг — решение на слух, а не запрет. Но знать о нём нужно до выбора, а не после.

Шаг 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 с в конце.

Тонкость: хвост и выстрел — одна запись, поэтому усиление у них должно быть одно (−1.1 дБ), иначе хвост «отклеится». Отрезок хвоста в записи пикует на −9.64 дБ → в рецепте хвоста "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. Сделай сам: замена сэмпла по шагам

Порядок, по которому прошли оба кейса. Каждая команда — из корня репозитория.

  1. Записи на местоПоложите банк в assets-src/audio/ или сделайте ссылку — копировать гигабайты не нужно.
    ln -s ~/Downloads/"Sonniss GDC 2019" assets-src/audio/sonniss-gdc-2019
    npm run sound -- catalog scan
  2. Найти и измерить кандидатов
    npm run sound -- catalog search machine gun --max 60
    npm run sound -- catalog measure machine gun --max 60
    Слушайте в темпе игры. Проверьте клиппинг (astats, раздел 4).
  3. Посмотреть атаки или платоВыстрелы — onsets; луп — ровный по уровню отрезок.
    npm run sound -- onsets "путь/к/записи.wav"
  4. Черновик и нарезка
    npm run sound -- add "путь/к/записи.wav" --as mg-shot --slices 8 --force   # выстрелы
    npm run sound -- add "путь/к/записи.wav" --as engine-low --loop --length 6 --force   # луп
    Или мышью: микшер → «Сэмплы» → вкладка «Исходник», маркеры на волне, Save.
  5. Поправить числа и перенарезатьВ src/data/sounds.json → samples.<id>.recipe: starts, length, loop, peak.
    npm run sound -- cut mg-shot
  6. ОписатьВ samples.<id>: description, sources (банк и файл), cut — словами, как нарезано.
  7. Подключить и подстроитьСэмпл в samples звука; у лупа — recorded под обороты записи. Под npm run dev игра подхватывает правку без перезагрузки: в консоли Sound: registry reloaded.
  8. СвестиГромкость старого и нового — LUFS; разницу — trim в окне «Сэмплы» (не перекодированием). Уровень звука — ⇧F11. Ctrl+S.
  9. Проверить и закоммитить
    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; редактор рецепта на волне записи
«Кривые»Кривые слоёвтермы громкости и высоты; оранжевая точка — значение в полёте

Какую ручку крутить

Фейдер канала

Вся группа: все выстрелы, вся музыка.

Уровень (⇧F11)

Один звук в его канале: mg.enemy тише своего.

Trim («Сэмплы»)

Одна запись внутри звука: новый луп под громкость старого.

Кривая («Кривые»)

Как громкость или высота следует за скоростью, оборотами, высотой.

Несохранённые правки — «● не сохранено» в тулбаре; они же в браузере, вкладку можно закрыть. «К файлу» — отбросить, 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.