Всеки звук в нашите 3D браузърни игри Faultwarden и Billowpost се създава от кода, докато играете: тътенът на удара, пукотът, който тича по разлома, стонът на каменна плоча, която всеки миг ще падне, звънът, когато вземете кристал, и музиката отдолу. В двете игри няма нито един записан звук. Така изтеглянето остава малко, всеки звук е малко различен всеки път, а ефектите следват тоналността на музиката. Така обаче е много лесно да се получи и остра, изкривена каша. Това ръководство показва шината за смесване, която използваме, как един звук се изгражда от слоеве и как автоматичен тест пази нивото в разумни граници.
#Звукът тръгва след действие на играча
Браузърите не пускат звук, докато човекът не взаимодейства със страницата. Създайте AudioContext при първото натискане на показалец или клавиш, а не при зареждането на страницата, и изградете целия граф в този момент. Когато разделът е скрит, спрете контекста временно, а когато се върне, го продължете. Спреният контекст не струва нищо и не може да изненада никого със звук от раздел на заден план.
const unlock = () => audio.unlock(); // създава контекста веднъж
addEventListener('pointerdown', unlock, { capture: true });
addEventListener('keydown', unlock, { capture: true });
#Една шина за всичко
Най-честата грешка при синтезиран звук е всеки осцилатор да се свърже направо към високоговорителите. Десет застъпващи се удара се събират, изходът минава пълната скала и браузърът го изрязва в пращене. Вместо това всеки източник минава през една верига. Ето тази във Faultwarden; Billowpost ползва същите части в малко по-различен ред:
ефекти ┐
музика ┼─▶ високочестотен филтър 35 Hz ─▶ компресор ─▶ нискочестотен филтър ─▶ усилване ─▶ главен изход ─▶ ограничител ─▶ меко изрязване ─▶ високоговорители
среда ┘ ▲
звуци на менюто ┘
- Високочестотен филтър на 35 Hz. Ниските синусоиди носят енергия, която никой не чува на говорителите на лаптоп, но компресорът реагира на нея и „помпа“ цялата смес. Срязването под 35 Hz запазва баса, който се чува, и маха останалото.
- Компресор на шината. Праг минус 16 dB, съотношение 4, мек преход от 10 dB, атака 4 ms и освобождаване 200 ms. Той слепва ефектите и музиката и сваля натоварените моменти, за да не заглушават всичко останало.
- Нискочестотен филтър. Обикновено е напълно отворен, на 18 kHz. Когато играта е на пауза, той се спуска и светът зад менюто звучи приглушено, което е евтин и ясен знак.
- Компенсиращо усилване. След компресията сместа е по-тиха, затова постоянно усилване от 2,25 (около 7 dB) я връща до нивото, към което се целим.
- Ограничител и меко изрязване. Бърз компресор на минус 3 dB със съотношение 20 хваща пиковете, а формирател на вълната като последно стъпало е линеен до 0,8 и след това се огъва плавно, така че изходът никога не надминава 0,94. Дори двайсет едновременни удара се насищат меко, вместо да пращят.
Звуците на менюто влизат след нискочестотния филтър, направо в главния изход, така че щракванията в менюто за пауза остават ясни, докато играта зад него е приглушена.
function safetyCurve() {
const n = 4096, lin = 0.8, ceil = 0.94;
const c = new Float32Array(n);
for (let i = 0; i < n; i++) {
const x = (i / (n - 1)) * 2 - 1;
const a = Math.abs(x);
const y = a <= lin ? a : lin + (ceil - lin) * Math.tanh((a - lin) / (ceil - lin));
c[i] = Math.sign(x) * y;
}
return c;
}
clipper.curve = safetyCurve();
clipper.oversample = '2x';
#Два градивни блока
Почти всеки ефект в нашите игри е няколко извиквания на две помощни функции. Едната пуска осцилатор, чиято честота се плъзга от една стойност към друга под кратка обвивка. Другата пуска филтриран шум, при който се плъзга честотата на филтъра.
tone(type, f0, f1, t, attack, decay, peak) {
const o = ctx.createOscillator();
o.type = type;
o.frequency.setValueAtTime(f0, t);
if (f1 !== f0) o.frequency.exponentialRampToValueAtTime(Math.max(20, f1), t + attack + decay);
const g = ctx.createGain();
g.gain.setValueAtTime(0.0001, t);
g.gain.exponentialRampToValueAtTime(peak, t + attack);
g.gain.exponentialRampToValueAtTime(0.0001, t + attack + decay);
o.connect(g).connect(sfxBus);
o.start(t);
o.stop(t + attack + decay + 0.05);
}
Експоненциалните преходи звучат естествено и за височината, и за силата, но не могат да започват или да свършват в нула, затова обвивката тръгва от 0,0001. Функцията за шум пуска четирисекунден буфер с бял шум, напълнен веднъж при старта, от случайна позиция и със случайна скорост между 0,8 и 1,2, така че два поредни удара с шум никога нямат едно и също зърно.
#Звукът е слоеве
Убедителният удар никога не е един осцилатор. Ударът по разлома във Faultwarden е пет слоя, които започват в рамките на 20 милисекунди един от друг:
case 'strike':
tone('sine', vary(95), 38, t, 0.004, 0.32, 0.5); // тяло: падащ нисък тътен
tone('triangle', vary(280), 120, t, 0.003, 0.14, 0.3); // почукване: средна плътност
tone('triangle', vary(160), 60, t, 0.002, 0.12, 0.25);
noiseHit(t, 'bandpass', vary(2200, 0.2), 900, 0.9, 0.004, 0.11, 0.45); // пукот
noiseHit(t + 0.02, 'lowpass', 1400, 300, 0.7, 0.01, 0.42, 0.4); // опашка от чакъл
break;
Синусоидата дава тежестта, която се усеща, триъгълните вълни дават почукването, което се чува и на говорител на телефон, който не може да възпроизведе 95 Hz, шумът през лентов филтър е пукотът, а шумът през нискочестотен филтър е опашката от чакъл. vary() мести всяка стойност до 8 процента нагоре или надолу, така че сто удара никога не звучат като един запис, пуснат отново и отново.
Някои звуци носят информация. Когато разломът отвори линия от плочи, всяка плоча пука 22 милисекунди след предишната и с 6 процента по-високо, така че играчът чува посоката и дължината на разреза. Звънът на кристала взима височината си от акорда, който музиката свири в момента, и се изкачва с един тон от акорда при всеки следващ кристал в серия, което прави събирането музикално, а не шумно.
#Прагове срещу натрупване
Когато шест врага паднат в един кадър, шест еднакви звука, тръгнали заедно, само добавят сила. Всеки ефект има праг, минимално време между две пускания: 30 милисекунди за удара, 55 за звъна на кристала, 300 за стона на задържаща се плоча. Второ извикване в рамките на прага просто се пропуска. Заедно с шината точно това пази най-натоварените моменти ясни.
#Измерете нивото с тест
Ушите свикват със сместа, а лаптопите лъжат за баса. Затова браузърните ни тестове мерят това, което наистина излиза от играта. AnalyserNode след меко изрязване чете изхода, а тестът от край до край изиграва отрязък от истинска игра и проверява две числа: медианата на силата (средноквадратичната стойност на отчетите) трябва да е между 0,04 и 0,14 по време на игра, а нито един пик не бива да стига 0,95. Същият тест натиска изключване на звука и проверява, че изходът замлъква. В Billowpost измерената смес по време на полет беше с медиана 0,096 и пикове до 0,51, а изключването на звука действаше за под 120 милисекунди.
За готови звукови файлове има истински стандарти за сила на звука с LUFS и истински пик; нашата статия за сила на звука, истински пик и безшевни цикли ги описва, а звуковият ни пакет Zaklinka е изравнен до тези цели за двигатели, които пускат файлове, вместо да синтезират.
#В какво синтезът е добър и в какво не
Синтезът е отличен за удари, пукоти, свистене, звънове, щраквания в интерфейса, фон от вятър и огън и за музика от прости гласове. Трудно му е с човешки глас, истински инструменти и сложни природни звуци като тълпа. Практическо правило: ако звукът може да се опише като „височина, която пада, плюс малко филтриран шум“, синтезирайте го; ако за описанието му трябва микрофон, запишете го или вземете лицензиран. Ще чуете и двата ни синтезирани саундтрака, когато игрите излязат; те са обявени като „Очаквайте скоро“ в каталога с игри на PN Scripts. За по-широката картина на усещането в играта вижте как браузърната игра да се усеща добре.
#Често задавани въпроси
Защо играта ми с Web Audio пращи, когато звучат много звуци?
Източниците се събират над пълната скала и изходът се изрязва. Пратете всичко през една шина с компресор, ограничител и меко изрязване като последно стъпало и ограничете колко често може да тръгва един и същ звук.
Защо exponentialRampToValueAtTime не може да стигне нула?
Експоненциалната крива никога не стига нула, затова спецификацията на Web Audio не позволява нула за цел. Стигнете до много малка стойност като 0,0001, която е далеч под прага на чуване, и спрете източника малко след това.
По-лек ли е синтезираният звук от аудио файловете?
Да. Кодът за всички ефекти и музиката в една от нашите игри е няколко десетки килобайта, докато сравним набор от компресирани файлове би бил мегабайти. Разходът се мести към процесора, а за няколко десетки кратки гласа той е малък.
Колко силна трябва да е браузърната игра?
Целим се в медиана на изхода между 0,04 и 0,14 по време на игра и пикове под 0,95 и го проверяваме с анализатор в автоматичните тестове. Винаги давайте на играчите отделна сила за музиката и ефектите и клавиш за изключване на звука.
Коментари
Коментари
Напишете първия коментар.
Оставете коментар