FLAGS
Home
Daily
Play
Multiplayer
Challenges
Leaderboards
World Activity
Social
Explorer
Learn
Help
Contact
Master 75%
UI scale
100%
All posts
audioengineeringdesign

Rebuilding Every Sound in flags.games: One Key, One Room, No Clicks

Our correct-answer chime was louder than winning a challenge. We measured 35 sound effects, found off-key beeps, square waves, clicks and late timing, and rebuilt them all on plain Web Audio. Play every before and after.

October 6, 2026 · 13 min read

Until this week, the loudest sound in a normal game of flags.games was the correct-answer chime. At −22.1 it was 7 louder than winning a whole challenge, and as loud as finishing the daily.

Nobody decided that. The site had grown 35 sound effects, each added on its own and tuned by ear in isolation. So we measured all of them, then rebuilt every one on a single small engine: one musical key, one shared room, and one rule for how every note starts and stops.

Every example below plays in your browser, so turn your sound on. The Before buttons play the old sounds, re-created from the old code.

Before and after

Both sides follow your Settings volume. Sound effects must be on.

  • Correct answer

    Heard every round. It was the loudest everyday sound on the site.

  • Wrong answer

    A bell with sour extra tones, 11 dB under the correct chime.

  • Countdown into a round

    Clock ticks, then a buzzy chord on every round.

  • Lobby start countdown

    Two beeps, both off the note. Both countdowns now share one sound.

  • Weekly spin jackpot

    The best prize on the site played C and C♯ in the same phrase.

Measuring 35 sounds

Ears adapt. After 10 minutes of auditioning, a harsh sound starts to feel normal and a quiet one starts to feel fine. So before changing anything, we built a way to measure.

A script loads each sound's code into headless Chromium and renders it with an at the default volume of 75%. Offline rendering runs faster than real time and produces the same samples on every run. Then the script measures each file:

  • Loudness as momentary LUFS. It tracks how loud a sound feels. Peak level only catches the tallest instant.
  • Brightness as the , plus how strong the content above 7,000 is.
  • Pitch: the strongest frequencies, named as notes, with how many each sits from the nearest piano key.
  • Clicks: the largest sudden jump in the waveform.

34 of the 35 sounds rendered. The 35th was an MP3 on a CDN our test machine couldn't reach. The result was a table with one row per sound, and reading it top to bottom was humbling.

What the measurements found

Loudness followed nothing

Game sounds need an order. A tap you hear constantly should be the quietest thing on the site, and a win you hear once a day should be the loudest. In between, loudness should rise with how rare and how important the moment is.

The old palette had no order at all:

  • A correct answer, heard every round, measured −22.1 LUFS. Winning a whole challenge measured −29.3.
  • A wrong answer measured −33.6, more than 11 dB under a correct one. Misses barely registered.
  • Switching the theme measured −21.7, louder than finishing the daily.
  • The multiplayer "last player standing" sting was the loudest sound on the site at −18.5.
  • The last-five-seconds tick, the one sound meant to raise your pulse, was among the quietest at −43.1.

Loudness, before and after

Momentary LUFS at 75% volume. Closer to zero (further right) is louder. Shaded bands are the 5 tiers.

Before After
  • Opponent answered −39.8 → −39.0

  • Stepper −38.9 → −39.5

  • Globe ping −27.3 → −39.1

  • Click −35.8 → −37.3

  • Selection −31.1 → −37.1

  • Player left −35.1 → −36.7

  • Toggle on −29.3 → −33.9

  • Last-second tick −43.1 → −33.9

  • Theme switch −21.7 → −33.5

  • Milestone −27.3 → −32.9

  • Start button −32.9 → −32.0

  • Round start −25.0 → −29.0

  • Wrong answer −33.6 → −28.5

  • Correct answer −22.1 → −27.0

  • Game lost −26.5 → −25.5

  • Challenge won −29.3 → −25.0

  • Last player standing −18.5 → −25.0

  • Spin jackpot −32.5 → −22.8

  • Daily finished −22.4 → −22.1

  • Game won new → −22.0

LUFS
−45−40−35−30−25−20
Show the numbers
CueBeforeAfter
Opponent answered−39.8−39.0
Stepper−38.9−39.5
Globe ping−27.3−39.1
Click−35.8−37.3
Selection−31.1−37.1
Player left−35.1−36.7
Toggle on−29.3−33.9
Last-second tick−43.1−33.9
Theme switch−21.7−33.5
Milestone−27.3−32.9
Start button−32.9−32.0
Round start−25.0−29.0
Wrong answer−33.6−28.5
Correct answer−22.1−27.0
Game lost−26.5−25.5
Challenge won−29.3−25.0
Last player standing−18.5−25.0
Spin jackpot−32.5−22.8
Daily finished−22.4−22.1
Game wonn/a−22.0

Each sound now sits in one of 5 tiers, and the tier depends on how often the sound fires:

TierLUFSSounds
Micro−40 to −37Taps, stepper, opponents answering, globe pings
Interface−36 to −32Toggles, theme switch, joins and leaves, milestones, countdown ticks
Feedback−29 to −27Correct, wrong, time up, round start
Outcome−27 to −25Challenge won or lost, game lost, last player standing
Finale−23 to −22Game won, daily finished, weekly spin jackpot

Correct and wrong now sit 1.5 dB apart. A miss is as clear as a hit, and it still doesn't sound like a buzzer.

Round numbers are out of tune

Most of the sounds were written as note names (C5, E5, G5), and most of those were in . The plain Web Audio sounds were written in hertz, and the hertz were round numbers:

  • Lobby countdown: 600 Hz for 3, 2, 1 and 800 Hz for go. 600 Hz is a D5 that's 37 cents sharp. 800 Hz is a G5 that's 35 cents sharp.
  • Stepper: 440 Hz going up, a perfect A4, and 380 Hz going down, an F♯4 that's 46 cents sharp. That's nearly halfway to G.
  • Player kicked: 300 Hz, a D4 that's 37 cents sharp.
  • Player joined: A4 then C♯5, a major third from A major. That C♯ clashes with the C natural in almost every other sound.
  • Weekly spin jackpot: C4, F4, A4, C♯5, E5. The best prize on the site played C and C♯ in the same phrase.

A cent is a hundredth of a . At 35 cents and up, a note is far enough off that it sounds sour next to anything in tune.

That matters because game sounds overlap all the time. The last countdown tick rings into the round's opening chord. An opponent's answer chime lands on top of yours. When every sound draws from the same few notes, an overlap makes a chord. When they don't, it makes a clash.

The sneakiest case was the wrong-answer bell. On paper it was in key: B4, G4, E4, an E minor arpeggio. It was an bell, which means a second oscillator wobbled the note's frequency at 1.5 times the note. FM adds at the note plus and minus multiples of that wobble, and with a 1.5 ratio one of them lands at 2.5 times the note. For E4 that's 824 Hz, a slightly flat G♯5. For B4 it's 1,235 Hz, a slightly flat D♯6.

So the sad E minor sound also played a G♯ over its own G. That's where its clangy, sour edge came from.

FM has a second trap. At a 1:1 ratio, one of those partials lands at 0 Hz, which is a . In our test, a 440 Hz FM tone at modulation index 1 averaged −0.23 where it should average 0. The new bell is additive: plain partials, each at exactly the ratio we choose.

Square waves on the busiest sound

The round-start sound plays every round, which makes it one of the most-heard sounds in the game. It was 4 notes, C5 E5 G5 C6, 60 ms apart.

A square wave stacks every odd harmonic, and they fade slowly: the 9th harmonic is still a ninth as strong as the note. So most of its energy sits far above the pitch you hear. Its spectral centroid was 4,220 Hz, 3 octaves above the C5 it started on, and its content above 7 peaked only 5.8 dB below the whole sound's peak.

The new round start is a soft strum of C5, G5 and C6 with a low felt thump under it. Its centroid is 820 Hz, and its content above 7 kHz sits 47.7 dB below the peak. The game-over sound had the same square-wave problem at 3,391 Hz and now sits at 536 Hz.

Clicks at both ends of a note

A click is a sudden jump in the waveform. A speaker can't follow a jump, so it plays a short burst of every frequency at once. The helper behind the stepper, the lobby countdown, joins, kicks and the replay beep made 2 clicks per note:

packages/audio/src/lite/lite-audio.ts (old)
const scaledVolume = 0.1 * globalVolume;
gainNode.gain.setValueAtTime(scaledVolume, ctx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(
  Math.max(0.001, scaledVolume * 0.1),
  ctx.currentTime + duration
);

oscillator.start(ctx.currentTime);
oscillator.stop(ctx.currentTime + duration);

The gain is at full level on the very first sample, so the note starts with a jump. Then it only decays to 10% of that level before stop() cuts the oscillator mid-cycle. That's the second jump.

The countdown tick had a gentler version: a 1 ms , short enough to splash a click across the spectrum. The milestone chime started with a hard onset straight into a big , so the reverb kept its click ringing.

7 reverbs and 2 audio contexts

Reverb tells your ear what kind of space a sound is in. The old palette used 7 different reverbs with different lengths and tones, and some sounds had none. Played one after another, they came from different rooms: a milestone chime rang in a big wet space, and the click right after it was bone dry.

Tone.js also ran its own next to the one the small sounds used. Each context has its own clock and its own output, so the 2 halves of the palette couldn't be scheduled against each other, and the ad-break mute only reached some of them.

Sounds that arrived late

The daily results screen reveals 5 columns, one per round, with a ping for each. On a cold start, the pings lagged the animation:

packages/audio/src/rich/tone-manager.ts (old)
const synth = await this.ensureDailySlotTickSynth();
const Tone = await this.getTone();
const now = Tone.now();

for (let k = 1; k <= roundCount; k++) {
  const revealAt = now + (initialDelayMs + k * stepMs) / 1000;
  synth.triggerAttackRelease(roots[k - 1], "32n", revealAt);
}
Daily results on a cold start, before every ping late by Tone's load time
  1. Columns start revealing

    0 ms

    The animation starts on mount, on its own timer.

  2. Sound waits for Tone.js

    then

    The ping code awaits the Tone.js import and its synth before scheduling anything.

  3. Tone.now() becomes zero

    + load time

    All 5 pings are scheduled from the moment Tone finished loading, so each lands late by the whole load.

  4. Same ping for every column

    always

    The schedule only knew how many rounds there were. Solved and missed sounded alike.

The fix is to time the pings from when the reveal started, measured with performance.now(), and to subtract however long audio took to wake:

packages/audio/src/lite/lite-audio.ts
await playCue((bus, at, volume) => {
  const elapsedMs = performance.now() - schedule.startedAtMs;
  schedule.solved.forEach((solved, index) => {
    const offsetMs =
      schedule.initialDelayMs + (index + 1) * schedule.stepMs - elapsedMs;
    if (offsetMs >= -40) {
      const slotAt = at + Math.max(0, offsetMs) / 1000;
      dailySlotCue(bus, slotAt, volume, index, solved);
    }
  });
});
Daily results now on the column, every time
  1. Reveal starts

    0 ms

    The results component records performance.now() as the stagger begins.

  2. Audio wakes

    first cue

    No library to load. The shared context resumes.

  3. Each ping lands on the audio clock

    then

    Its time is the reveal start plus its delay, minus the time audio took to wake.

  4. Solved climbs, missed taps

    always

    Solved rounds step up a bright ladder; missed rounds tap a muted one an octave lower. Columns already shown are skipped.

The other timing fixes:

  • The first answer waited for a download. Correct and wrong lived in the Tone.js bundle, so the first one in a session waited for it. The old code even logged the wait as import_wait_ms.
  • Weekly spin ticks drifted. Every peg tick was its own setTimeout, and timers slip when the main thread is busy, which it is while a wheel spins. Tick times now come from the wheel's own easing curve and are all scheduled on the audio clock before the wheel moves.
  • Running out of time was silent. The last 5 seconds ticked, then nothing. A new time-up sound lands the countdown.
  • Survival mode doubled up. On your last heart, the wrong-answer sound and the game-over sound started on the same frame. Game over now waits 650 ms.
  • Phrases used timers. Some multi-note sounds spaced their notes with timers. Every note of every sound is now scheduled on the audio clock.
  • An unused import loaded all of Tone. The daily results component imported the old audio module without calling it, which was enough to pull Tone.js into that page.

How the new sounds work

The new engine is about 1,600 lines of plain Web Audio in 5 files: a score with every sound, the instruments, the shared room, a player, and the note names.

One key

Every pitch is a natural note (C, D, E, F, G, A or B), so everything is in C major. Most sounds use only 5 of those: the C major , C D E G A. The pentatonic has no half steps, so any of its notes sound fine together. Many wind chimes are tuned to it for the same reason.

Sad sounds use the same 7 notes and lean on the minor chords inside the key, A minor and E minor. A wrong answer sounds like a gentle letdown and still sits in the same key as everything around it.

The note type only accepts naturals, so a sharp can't sneak in through a typo or a round number:

packages/audio/src/sfx/notes.ts
type Letter = "C" | "D" | "E" | "F" | "G" | "A" | "B";
type Octave = 2 | 3 | 4 | 5 | 6 | 7;

export type Note = `${Letter}${Octave}`;

export function hz(note: Note): number {
  const letter = note.charAt(0) as Letter;
  const octave = Number(note.slice(1));
  return 440 * 2 ** ((SEMITONES_FROM_A[letter] + (octave - 4) * 12) / 12);
}

hz("C#5") is a compile error. There's one deliberate bend in the whole palette: the second note of a wrong answer drifts 35 cents flat over 350 ms as it fades, like a voice saying "aww". It starts in tune and only sags on the way out.

A small band of instruments

Every sound is built from 6 voices. Reusing a few instruments everywhere is a big part of why the sounds feel related.

  • Glass: a soft celesta or music box. A sine fundamental, a quieter octave and twelfth that fade faster, and a faint metallic tine for the first few milliseconds. Longer notes add a slow chorus pair, 3.5 cents either side, spread across the stereo field. Correct answers, chimes and wins.
  • Wood: a soft marimba tap. A sine body with a quick downward mallet bend and a 2 ms burst of filtered noise for contact, which makes it feel tactile. Clicks, ticks and steppers.
  • Felt: a low, round thump whose pitch falls into the note. An octave partial keeps it audible on phone speakers that can't play the fundamental. Reveals, kicks, and the floor under big chords.
  • Pad: 2 triangle oscillators per note, detuned 7 cents either way, through a slowly opening low-pass. Finales and long streaks.
  • Air: filtered noise with a moving filter, for whooshes and breaths.
  • Flick: a few milliseconds of band-limited noise, like a card edge. The avatar shuffle.

Here's the correct-answer sound in full, minus the 2 bigger streak layers:

packages/audio/src/sfx/cues.ts
export function successCue(bus: SfxBus, at: number, volume: number, streak: number) {
  const cue = open(bus, "success", volume, 0.18);
  glass(cue, { at, frequency: hz("E5"), peak: 0.75, decay: 0.12 });
  glass(cue, { at: at + 0.055, frequency: hz("G5"), peak: 0.8, decay: 0.14 });
  glass(cue, { at: at + 0.11, frequency: hz("C6"), peak: 0.95, decay: 0.27 });
  if (streak >= 3) {
    glass(cue, { at: at + 0.165, frequency: hz("E6"), peak: 0.42, decay: 0.3 });
  }
  // 6+ adds G6, 10+ adds a warm C-G pad underneath
}

One room

Every sound sends a little signal into one shared reverb, so the whole palette sounds like it's in the same room. The room is generated in code: 2.4 seconds of stereo noise that decays by 60 dB in 1.55 s, after an 11 ms . A filter on that noise closes from 6.5 kHz to 1.1 kHz over the tail, so echoes get warmer as they fade and never hiss. A 200 Hz on the return keeps low thumps from turning to mud.

A soft clipper sits after the room. It's linear up to about −4.4 and only rounds off peaks when several sounds pile up at once.

Every note starts and ends at zero

packages/audio/src/sfx/voices.ts
/** e^-9 ≈ -78 dB: an envelope this many time constants into its decay is silent. */
const SILENT_AFTER_TAUS = 9;

gain.gain.setValueAtTime(0, at);
gain.gain.linearRampToValueAtTime(peak, at + attack);
gain.gain.setTargetAtTime(0, at + attack, decay);

oscillator.start(at);
oscillator.stop(at + attack + decay * SILENT_AFTER_TAUS);

Every partial of every voice goes through this . It starts at exactly 0, ramps to its peak in a few milliseconds, then decays exponentially. The oscillator stops only after 9 , when the level is e−9 of the peak, about −78 dB. At that level there's nothing left to cut, so there's nothing to click.

Levels set by measurement

Each sound has one level in a table, in decibels. To set them, a calibration script rendered every sound, measured its momentary loudness and moved its level toward the middle of its tier. After a few passes every sound landed within about 0.4 dB of target.

Streaks are the one place we kept the level flat on purpose. A streak of 3 adds an E6 on top of the correct chime, 6 adds a G6, and 10 adds a warm pad underneath. Measured loudness barely moves (−27.0 to −26.3), so a long run sounds richer without getting tiring.

Sounds that know what's happening

A sound that repeats identically starts to grate. Real instruments never play the same note exactly the same way twice, so the new sounds vary in small ways. The click shifts a few cents and a fraction of a decibel each time, and several sounds react to what's going on in the game:

Sounds in context

Each button plays a short sequence the way the game would.

  • Answer streak

    Correct at streaks of 1, 3, 6 and 10. More sparkle, about the same level.

  • Opponents answering

    Each answer in a round climbs one note, like rain starting.

  • Stepper drag

    The detent's pitch follows the value, so a drag up and back plays a scale.

  • Daily results

    Solved, missed, solved, solved, missed. Solved climbs, missed taps.

  • Globe activity

    Wind chimes: a random pentatonic note, never the same one twice in a row.

  • Reveals

    Quick repeats climb a step each, then start over after a pause.

  • Last 5 seconds

    Ticks lean in as time runs out, then time up lands the countdown.

Dropping Tone.js

We were loading about 270 KB of Tone.js, plus a second audio context, to play short stings. The new engine is 21 KB, 7 KB gzipped, and the production build no longer contains Tone at all.

One context means one clock and one output. Recorded voice clips now decode onto the same bus, so the Settings volume and the ad-break mute reach every sound. The mute is a short fade, so ringing tails don't click off.

The recorder lied

One detour is worth writing down. Our first measurements recorded sounds in real time with a , and the recordings had clicks in them, even in sounds that couldn't click.

The clicks sat on 2,048-sample boundaries, the recorder's buffer size. Under load the capture dropped buffers, and the splices around each gap looked just like clicks in the sound. Offline rendering computes every sample without a real-time deadline, and the phantom clicks went away. Every number in this post comes from offline renders.

Tests that listen for us

A fake Web Audio implementation records every oscillator, every gain automation, and every start and stop time. The tests play every sound into it:

packages/audio/src/sfx/cues.test.ts
it.each(CUES)("%s starts from silence and is silent before its voices stop", async (_, play) => {
  for (const source of await sourcesFrom(play)) {
    const envelope = envelopeFor(source);
    expect(source.startTime).toBeGreaterThanOrEqual(fakeContext().currentTime);
    expect(envelope.valueAt(source.startTime)).toBeLessThanOrEqual(1e-9);
    expect(envelope.valueAt(source.stopTime)).toBeLessThanOrEqual(envelope.peak * 1e-3);
  }
});

This one checks that every voice starts from silence and is at least 60 dB below its peak when it stops. A second test checks that every oscillator plays a natural note, or a known partial of one, within 12 cents.

To prove the tests can fail, we broke the engine on purpose, stopping voices after 3 time constants where it should wait 9, and 33 of the 71 sound tests failed.

Every change in one table

Show all 28 changes
SoundBeforeAfter
Click2 notes, a little boopyOne soft wooden tap on A5 that varies slightly
Stepper440 Hz and an out-of-tune 380 Hz, clickingWooden detent that plays a scale
Countdown ticksNearly inaudible, 1 ms attackG5 tick that leans in each second
Time upSilentNew: the tick stops and a soft fall lands it
Lobby countdown600 and 800 Hz beeps, 35 to 37 cents sharpSame ticks and start chord as rounds
Round startSquare-wave arpeggioSoft C chord strum over a low thump
TogglesWaited for Tone.js on first useOn lifts a fifth, off settles a fifth
Theme switchAs loud as the daily finaleFalling dusk into dark, rising sunrise into light
MilestoneHard onset into a big reverb2 soft chime notes a fifth apart
Globe activityThe milestone chime, every timeOne quiet wind-chime note
Opponent answeredA5 and E6, identical every timeClimbs one note per answer
Player joinedA major third, out of keyLifts a fifth
Player kicked300 Hz, 37 cents sharpLow muted knock
Avatar shuffleHarsh white-noise clicksCard flicks that slow and land on a chime
RevealsThe same thump every timeQuick repeats climb a step
Replay resultsClicky beep, cut offSoft open fifth
CorrectLoudest everyday soundE5 G5 C6 roll; streaks add sparkle
WrongSour FM bell, 11 dB under correctFalling minor third that sags flat
Challenge wonQuieter than one correct answerPickup into a bell chord over a pad
Challenge lostOpened on a high alert noteGentle A minor fall
Last player standingLoudest sound on the siteFast cascade that lands softly
Daily columnsSame ping for solved and missed, lateSolved climbs, missed taps, on time
Daily finishedAlready in CSlower, warmer chord with a pad swell
Game wonAn MP3 from a CDNSynthesized fanfare in the same palette
Game lostSquare-wave womp wompA kind phrase that comes to rest
Survival game overFired with the wrong-answer soundWaits 650 ms
Weekly spinTimer ticks that driftedMusic-box ticks locked to the wheel
Spin prizesClashing chords, similar levelsConsonant phrases that grow by tier

Rules for every new sound

These now live in the repo next to the code, and every new sound has to pass them:

  1. Stay in the key. Use the Note type. Reach for the pentatonic first (C D E G A). For sad moments, lean on A minor or E minor.
  2. Build from the existing voices. Glass, wood, felt, pad, air and flick. A new instrument needs a reason, and it goes through the same room.
  3. Start at zero, end in silence. Use the shared envelope. Never jump to a peak, and never stop a voice before 9 time constants.
  4. Pick the tier by how often it fires. Constant sounds go in micro or interface; once-a-game sounds go in outcome or finale. Then calibrate the level offline.
  5. Schedule on the audio clock. Space notes with at + offset, never timers. When a sound follows an animation, take its times from the animation's constants and its start time.
  6. One focal moment at a time. If 2 big sounds can fire together, like a wrong answer and game over, delay one.
  7. Vary what repeats. A few cents and a fraction of a decibel of variation, or a ladder that climbs with quick repeats.
  8. Let pitch carry meaning. Up is good, down is bad, higher is lighter. Pairs like on and off, join and leave, or solved and missed should mirror each other.
  9. Measure, then listen. Render offline, check loudness and brightness, then play it next to its neighbors.

Every sound in the game, plus full sequences and all 4 weekly spin tiers, is on the audio demo page. If one of them still bothers you, tell us. Now we can measure it.

Glossary

Every highlighted word in this post, in the order it first appears. Hover or tap one in the text to read it in place.

LUFS
Loudness Units relative to Full Scale: how loud a sound feels to people, measured with ITU-R BS.1770, the standard streaming services use. One LU is one dB. Always negative here. Closer to 0 is louder, so −22 is louder than −30. Neither is better on its own: each sound should sit at the right level for its moment.
Decibel (dB)
A step on a logarithmic loudness scale, always relative to something. Adding about 6 dB doubles a signal's level; about 10 dB sounds roughly twice as loud. Negative values mean quieter than the reference, so −30 dB is quieter than −20 dB.
OfflineAudioContext
The same engine rendering into memory instead of to speakers: faster than real time, and the same result every run.
Momentary loudness
LUFS measured over a sliding 400 ms window. It suits short sounds; we report the loudest window of each one.
Spectral centroid
The average frequency of a sound, weighted by how much energy each frequency has. A rough brightness meter. Higher is brighter. Short game sounds start to feel harsh once it climbs into the thousands of hertz.
Hertz (Hz)
Vibrations per second, which sets a sound's pitch. 440 Hz is the A above middle C; 1 kHz is 1,000 Hz. Higher is higher-pitched. Doubling the hertz raises a note by one octave.
Cent
A hundredth of a semitone, used to measure how far a note is from being in tune. A few cents off is hard to hear. By 35 cents a note sounds sour next to one that is in tune.
Tone.js
A popular JavaScript library for making music in the browser, built on Web Audio.
Key (C major)
The set of notes a piece uses and the one that feels like home. C major is the seven white keys, C D E F G A B, with C as home. A minor uses the same notes with A as home.
Semitone
The smallest step on a piano: from one key to the very next, black keys included. Twelve make an octave.
FM synthesis
Frequency modulation: one oscillator wobbles another's pitch very fast, which adds new partials. It makes bells easily, but the partials only land in key at certain ratios.
Harmonic (partial)
The quieter tones stacked above the note you hear, at multiples of its frequency. Their mix is why a flute and a violin playing the same note sound different.
DC offset
A waveform that sits off center instead of swinging evenly around zero. It wastes headroom and can thump when the sound starts or stops.
Sine wave
A pure tone with no harmonics at all: soft, round and a little plain.
Square wave
A tone that flips between two levels. It carries every odd harmonic, each only slowly quieter than the last, so it sounds bright and buzzy, like retro game music.
Attack
How long a note takes to reach full volume after it starts. Around a millisecond or less, the start itself is heard as a click.
Reverb
The tail of echoes a room adds to a sound. Its length and tone tell your ear what kind of space the sound is in.
AudioContext
The browser's Web Audio engine. It has its own clock, separate from timers, and sounds can be scheduled on it to the sample.
Pentatonic scale
A five-note scale. In C it is C, D, E, G and A. It leaves out the two notes that clash most, so any of its notes sound fine together.
Pre-delay
The short gap before a reverb's first echo. A few milliseconds keeps the start of each note clear.
High-pass and low-pass filters
A high-pass filter keeps the frequencies above a cutoff and removes the lows. A low-pass does the opposite.
Soft clipper
A safety stage that rounds off peaks gently instead of chopping them flat, so loud overlapping sounds don't crackle.
dBFS
Decibels relative to the loudest sample the system can output, which is 0 dBFS. Everything is at or below 0. −4.4 dBFS is a little under the ceiling.
Envelope
How a note's volume changes over time: how fast it rises (the attack) and how it fades (the decay).
Time constant
For a fading sound, the time it takes to fall to about 37% of where it was. Each further time constant takes it down by the same share again. After 9 time constants a sound is about 78 dB down: silent.
ScriptProcessorNode
An old Web Audio node that hands raw audio to JavaScript in real time. When the page is busy it can miss buffers.
Previous article
After Instant: Chunk Retries, Usable Markers, and the P95 Tail 7 min read
All articles Practice in Learn mode
© 2026 flags.games · About · Changelog · Terms · Privacy · Brand