Optimization of Sounds in Web Games

Sourcing

I hunt for music and SFX and drop everything into a single folder.

My favorite sources:

Decent:

When desperate:

“Combing” the files

What happens after you download a hundred files from everywhere? They all have different levels and dynamics. If you play them back-to-back, your brain tries to “normalize” on its own and you get ear fatigue. So I unify my files — then music flows track-to-track, and SFX blend better.

Start by normalizing

Two main approaches:

  • Peak normalization (raise track gain until it hits a target peak).

  • RMS / LUFS / LKFS / dBFS (based on average loudness / perceived loudness).

I use the second approach. It’s less visually obvious than peak, but much nicer in practice. The acronyms just differ by the exact metric your tool uses.

Reference points I keep handy:

  • RMS of Hollywood soundtrack final mixes is usually no hotter than −20 dB, some are closer to −24 dB (modern mainstream around −24 LKFS).

  • Tiny phone speakers will clamp everything once RMS exceeds about −8 dB.

  • Real-world game examples (integrated loudness):

    • Ryse: Son of Rome −23 LUFS

    • Bioshock Infinite −11.8 LKFS

    • Portal 2 −14.9 LKFS

    • Tomb Raider −16.7 LKFS

    • The Walking Dead −16.8 LKFS

    • Arkham City −17.1 LKFS

    • Borderlands 2 −19.6 LKFS

    • GTA IV −20.6 LKFS

    • Dishonored −23.1 LKFS

    • Skyrim −26.0 LKFS

My typical targets:

  • Music: −16 dB

  • Ambience: −16 dB

  • Dialogue: −12 dB

  • SFX: −16 dB

  • UI: −16 dB

For the web, setting most things to −16 dB is often the easiest — audio is secondary and needs to “sit.”

Loudness Range (LRA)

The more the loud/quiet parts differ within a track, the higher the Loudness Range.

Game examples (LRA):

  • GTA IV 18.3 LU

  • The Walking Dead 17.8 LU

  • Skyrim 17.3 LU

  • Borderlands 2 16.4 LU

  • Tomb Raider 14.9 LU

  • Portal 2 13.9 LU

  • Bioshock Infinite 13.2 LU

  • Dishonored 12.6 LU

  • Arkham City 10.6 LU

Wwise guidelines by listening environment:

  • Home theater: 20

  • Living room: 18

  • Kitchen: 15

  • Living room at night: 9

  • On public transport (mobile): 6

Rule of thumb: the louder the listening environment, the lower you can go with LRA. Genre and context matter a lot.

A quiet mix with low LRA at −23 LKFS can feel more tiring than a louder mix with a healthy LRA. Players turn loud games down and quiet games up; if dynamics are crushed, it’ll sound fatiguing either way.

Sorting

I sort downloads into five folders: Tier_1Tier_5.
Tier 1 = top-quality (music), Tier 5 = least important SFX.

Then a one-click batch converts everything into a Final folder:

@echo off
setlocal enabledelayedexpansion

REM Input folders
set TIER_1=Tier_1
set TIER_2=Tier_2
set TIER_3=Tier_3
set TIER_4=Tier_4
set TIER_5=Tier_5
set FINAL=Final

for %%f in (%TIER_1%\*.*) do (
    ffmpeg -i "%%f" -c:a libvorbis -q:a 10 -ar 44100 "%FINAL%\%%~nf.ogg")

for %%f in (%TIER_2%\*.*) do (
    ffmpeg -i "%%f" -c:a libvorbis -q:a 7 -ar 32100 "%FINAL%\%%~nf.ogg")

for %%f in (%TIER_3%\*.*) do (
    ffmpeg -i "%%f" -c:a libvorbis -q:a 5 -ar 32100 "%FINAL%\%%~nf.ogg")

for %%f in (%TIER_4%\*.*) do (
    ffmpeg -i "%%f" -c:a libvorbis -q:a 5 -ar 22050 "%FINAL%\%%~nf.ogg")

for %%f in (%TIER_5%\*.*) do (
    ffmpeg -i "%%f" -ac 1 -c:a libvorbis -q:a 3 -ar 16000 "%FINAL%\%%~nf.ogg")

echo done.
pause

Save as convert_to_ogg_web.bat. For mobile/PC, I keep separate batch files with different parameters.

In the end I have five folders of original, normalized, high-res files, and one Final folder optimized for the web (or whatever platform).

Same idea in Python, with loudnorm and a little reporting:

import subprocess, glob, os

LOUDNESS_TARGET = -16  # Integrated Loudness (I)
TRUE_PEAK = -1         # True Peak (TP)
LOUDNESS_RANGE = 16    # Loudness Range (LRA)

folders = {
    'Tier_1': {'q': 10, 'ar': 44100},
    'Tier_2': {'q': 7,  'ar': 32100},
    'Tier_3': {'q': 5,  'ar': 32100},
    'Tier_4': {'q': 5,  'ar': 22050},
    'Tier_5': {'ac': 1, 'q': 3, 'ar': 16000},
}
final_folder = 'Final'
os.makedirs(final_folder, exist_ok=True)

def size(path): return os.path.getsize(path)
def pct_change(orig, new): return round(((new - orig) / orig) * 100, 2)

total_orig = total_conv = 0

for folder, params in folders.items():
    for file_path in glob.glob(f'{folder}/*.*'):
        orig = size(file_path); total_orig += orig
        name = os.path.splitext(os.path.basename(file_path))[0]
        out  = f'{final_folder}/{name}.ogg'

        cmd = [
            'ffmpeg', '-loglevel', 'error', '-i', file_path,
            '-c:a', 'libvorbis', '-filter:a',
            f'loudnorm=I={LOUDNESS_TARGET}:TP={TRUE_PEAK}:LRA={LOUDNESS_RANGE}'
        ]
        if 'ac' in params: cmd += ['-ac', str(params['ac'])]
        cmd += ['-q:a', str(params['q']), '-ar', str(params['ar']), out]

        subprocess.run(cmd, check=True)

        conv = size(out); total_conv += conv
        print(f'{name}: size changed by {pct_change(orig, conv)}%')

print('\nTotals:')
print(f'Original: {total_orig/(1024*1024):.2f} MB')
print(f'Converted: {total_conv/(1024*1024):.2f} MB')
print(f'Reduction: {(total_orig-total_conv)/(1024*1024):.2f} MB')
print('done.')

What actually works for me?

Every project is different, so I sometimes hand-tune music. I also keep a helper batch that creates ~200 versions of a track across bitrate / sample rate / channel count combinations. Then I A/B and pick the best quality-per-MB tradeoff.

It takes every file in Music_test and writes all variants into Final:

@echo off
setlocal enabledelayedexpansion

set SOURCE_FOLDER=Music_test
set OUTPUT_FOLDER=Final
set ERROR_LOG=error_log.txt

if not exist "%OUTPUT_FOLDER%" mkdir "%OUTPUT_FOLDER%"

set QUALITIES=1 2 3 4 5 6 7 8 9 10
set SAMPLE_RATES=8000 11025 16000 22050 32000 44100 48000 88200 96000
set CHANNELS=1 2

for %%f in ("%SOURCE_FOLDER%\*.*") do (
  for %%q in (%QUALITIES%) do (
    for %%s in (%SAMPLE_RATES%) do (
      for %%c in (%CHANNELS%) do (
        echo converting %%~nxf q=%%q sr=%%s ch=%%c
        ffmpeg -i "%%f" -ac %%c -c:a libvorbis -q:a %%q -ar %%s ^
          "%OUTPUT_FOLDER%\%%~nf_q%%q_s%%s_c%%cch.ogg"
      )
    )
  )
)

echo done
pause

A quick “size intuition” table

For a one-minute casual loop, I typically see something like this:

  • Want ~1 MB?
    Either q=1 (~64 kbit/s) at almost any sample rate, or q=10 (~500 kbit/s) at 11,025 Hz.

  • For casual web games I usually live between q=4–6 and 22,050 Hz+.

Pearson correlations I saw on my test set (feature → file size):

  • Sample rate → size: 0.47

  • Mono vs stereo → size: 0.34

  • Bitrate (quality) → size: 0.55

So bitrate and sample rate dominate file size — but your ears should make the final call.

Sources

https://blog.audiokinetic.com/en/loudness-processing-best-practices-series-chapter1-loudness-measurement-part1/
https://blog.audiokinetic.com/en/loudness-processing-best-practices-chapter-2-loudness-dynamics-and-how-to-process-them/
https://blog.audiokinetic.com/en/loudness-processing-best-practice-chapter-3-scalable-loudness-processing-for-games/
https://designingsound.org/2013/02/20/different-loudness-ranges-for-console-and-mobile-games/
https://mcvuk.com/development-news/audio-loudness-for-gaming-the-battle-against-ear-fatigue/
https://www.stephenschappler.com/2013/07/26/listening-for-loudness-in-video-games/

14 Likes