Major update Devlog


Engine Sound Lab — Devlog

A record of the five priority improvements from the Standard Edition roadmap, in the order they were built. Each entry covers what changed, why it mattered, and the key implementation notes.

Devlog 1 — Rev‑Limiter Bounce

Version: v1.8.2 Priority: #1 from the roadmap Effort: Small Impact: Huge — instantly makes the engine feel alive

Before this, hitting the redline just clipped the RPM. The needle would sit flat at max and the sound would stay at one pitch. It read as a hard ceiling, not a limiter.

What changed

  • Added a fuel-cut state machine to the engine tick. When the RPM target reaches 98% of redline and throttle is above 55%, the limiter engages.
  • While engaged, a cycle runs: fuel cut → RPM drops → fuel on → RPM climbs → repeat.
  • Each fuel cut triggers a triggerLimiterPop() — a sharp crackle plus a low thud — mimicking a real ignition cut.
  • Limiter Bounce slider (1–12% of max RPM).
  • limiter>span class=""> state, and the whole container gains a rapid shake animation.

Key implementation notes

  • Cut duration: 45 ms. Full cycle: 85 ms (~12 Hz). Both are tuned to match the auditory "brap-brap-brap" of a real limiter.
  • RPM smoothing rate jumps from 0.012–0.045 to 0.45 during bounce, so the drop is audible in real time rather than smoothed away.
  • The engine gain dips to 18% during a cut so the pop punch stands out.

Result

 It audibly bounces, visually shakes, and physically drops and recovers RPM. This alone was the single biggest realism boost in the whole Standard edition.

Devlog 2 — Downshift Blip

Version: v1.8.3 (shipped with a full audio re-voice as well) Priority: #2 from the roadmap Effort: Small Impact: Huge — realism per line of code

Downshifting in Drive and Drag modes just dropped the gear. Real drivers blip the throttle on a downshift to match revs (heel-toe), and sportbike riders do the same. Without it, the downshift felt like a menu selection, not a mechanical action.

What changed

  • A startDownshiftBlip() method fires whenever shiftDown() is called in Drive or Drag mode and the car is moving above ~4 mph.
  • >span class="">bell-curve throttle envelope over 200 ms: rises to a peak, falls back to zero. The peak scales with the new Downshift Blip slider (0–100%, 0 = disabled).
  • >span class="">RPM overshoot on top of the target RPM, so the tach needle actually flares up during the blip.
  • A dedicated playBlipSound() emits a short band-passed noise burst, sweeping from 480 Hz up to 1500 Hz — a characteristic "bwap".
  • Status badge changes to Blip while the blip is active, and the toast on downshift reads ⚙️ Gear 3 · 🦶 Blip.

Key implementation notes

  • Blip duration: 200 ms. The envelope is sin(π · progress), so it's smooth in and out — no clicks.
  • RPM overshoot is capped at 0.05 + strength × 0.11 of the RPM range, so even at 100% it stays musical.
  • The blip only fires above 4 mph. Below that, downshifting to a stop wouldn't blip in a real car either.
  • Also part of this version: a major audio re-voice. The old top end was harsh; v1.8.3 added a tone chain (sub-sonic HPF, tone LP, de-harsh peak at 2.6 kHz, air shelf at 6.5 kHz, compressor) so the whole palette sounds warmer.

Result

Downshifting now has rhythm. Combined with the tone re-voice, the whole soundtrack became less synthetic and more like a real car or bike.

Devlog 3 — Per‑Cylinder Firing Variation

Version: v1.8.4 (also shipped an RPM-needle sync fix) Priority: #3 from the roadmap Effort: Medium Impact: High — every engine now has a fingerprint

Before this, every engine was built from the same 4 detuned saw oscillators. A V12 sounded like a sped-up V8. An I4 sounded like a slightly smaller V8. There was no character — no sense that each cylinder fires with its own tiny timing and gain variance.

What changed

  • Each engine now gets a stable cylinder fingerprint derived from a deterministic hash of its id + cylinder count. The fingerprint contains:
    • Four detune values (in cents) — one per oscillator.
    • Four gain multipliers — one per oscillator.
  • Detune spread scales with cylinder count — an I4 gets a tight ±5c spread, a V12 gets a wider ±12c spread. Higher cylinder counts audibly breathe more.
  • A crank-cycle phase (rpm / 120 Hz) drives a subtle amplitude wobble on each oscillator, with a different phase offset per oscillator. This mimics different cylinders sitting at different points in the firing order.
  • Wobble depth is scaled by cylinder count × cam profile and fades out toward the redline so high-RPM never sounds wobbly.
  • A new Cylinder Variation slider (0–100%) in the tuning panel lets you dial from completely balanced (0%) to full character (100%).

Key implementation notes

  • Each oscillator now has its own gain node (oscGain1…4) so the AM wobble can modulate them independently.
  • The four AM envelopes are phase-offset by 0, 0.25, 0.50, 0.75 of the crank cycle — enough to sound different but still musically coherent.
  • The sub-oscillator's AM depth is only 55% of the others, so low-end stays solid.
  • Same version also shipped the RPM needle sync fix: the loop now computes one authoritative RPM per frame and feeds the exact same integer to both gauge.setRpm() and updateUI(). The needle and digital readout can no longer disagree.

Result

A V8 sounds like a V8. A V12 sounds like a V12. A V-twin sounds like a V-twin. The synth stopped sounding like one engine with a pitch knob.

Devlog 4 — Recent Runs History

Version: v1.8.5 Priority: #4 from the roadmap Effort: Small Impact: Big engagement boost

>span class="">Every completed 0–60 or 1/4-mile run was lost the moment it finished. There was no way to compare your last attempt against the one before. No personal best. No sense of progress.

What changed

  • A new Recent Runs panel below the presets shows your last 5 completed runs for both the 0–60 sprint and the 1/4 mile.
  • Each entry shows:
    • >span class="">time in seconds.
    • For 0–60: the engine and the gear you finished in.
    • For 1/4 mile: the engine and the trap speed in mph.
  • Your personal best in each category is highlighted with a green-tinted row and a ★ prefix.
  • Beating a previous best fires a green-themed toast: 🏆 New best 0‑60: 5.42s.
  • A small ✕ button in the panel header clears the history, with a confirm prompt.

Key implementation notes

  • History is stored in localStorage under engineSoundLabHistory_v1, capped at 5 entries per category, and survives page reloads.
  • onZeroToSixty>span class=""> and onQuarterMile. The app wires these once during init(), and they fire the moment the finish line is crossed.
  • Existing engine timers, drag logic, and the callback placement are unchanged — the new hooks are purely additive.
  • isBest>span class=""> flag is recomputed on every insert against the trimmed list, so it stays correct after the list rolls over.

Result

Runs now have stakes. You can see the improvement. You know when you've beaten your own best. Small UI, big behavioral change.

Devlog 5 — PWA / Offline Mode

Version: v1.8.6 Priority: #5 from the roadmap Effort: Medium Impact: Makes it feel like a real app

The app ran in a browser tab. No install option. No home-screen icon. No offline support. Every launch felt like visiting a website, not opening an app.

What changed

PWA / installability

  • A full web app manifest is generated at runtime and injected as a data: URL — the app stays a single file.
  • Icons are drawn inline as SVG (concentric gauge rings in the app's orange gradient), Base64-encoded at runtime, and used for the favicon, apple-touch-icon, apple-touch-icon-precomposed, and manifest icons at 192 and 512 (both any and maskable).
  • iOS meta tags (apple-mobile-web-app-capable, status bar style, title) are set so a home-screen launch gives a native-feeling full-screen experience.
  • A beforeinstallprompt listener is registered in the <head> before anything else, so the prompt is captured the instant the browser fires it. A custom installpromptready event re-broadcasts it to the app.

Install button + help

  • An button appears in the header (and on the front menu) whenever the app is installable — or, on iOS where no install prompt exists, a hint button appears.
  • A dedicated walks the user through the correct steps for their platform: Chrome/Edge desktop, Chrome on Android, iOS Safari, Firefox, and a generic fallback. It automatically shows a ✓ Already Installed state when running in standalone mode.

Offline awareness

  • An OFFLINE pill appears in the header when navigator.onLine === false, and a toast fires on both transitions. Since the entire app is a single self-contained HTML file with zero external dependencies, everything keeps working offline.

Standalone detection

  • display-mode: standalone/fullscreen/minimal-ui>span class=""> (and the iOS navigator.standalone flag) and adds a standalone class to <body>, which shows a subtle ✓ Installed pill in the header.

Service worker (graceful)

  • A service worker registration is attempted at startup pointing at ./sw.js. Since a single-file app can't host its own SW, this silently fails in that case — but if you ever drop a sw.js next to the HTML, the app picks it up automatically and gains full SW-based offline caching on top of the manifest-driven install.

Key implementation notes

  • btoa(unescape(encodeURIComponent(svg)))>span class="">, then prepending data:image/svg+xml;base64,.
  • <meta name="theme-color"> is split by prefers-color-scheme so the browser UI chrome matches the light/dark theme.
  • The install button visibility refresh is idempotent — it can be called any number of times and stays correct.

Result

The app installs like a native app on desktop and mobile. It launches in its own window, works offline, and has a home-screen icon. It stopped feeling like a website and started feeling like software.

Bonus — What came after the roadmap

Once all five priorities were shipped, three more builds landed as extras:

v1.8.7 — Motorcycle engines

  • Added two bikes: I4 SS (600cc inline-four, 16,000 RPM redline, high-idle) and V-Twin (1200cc, 6,000 RPM redline, lumpy low-RPM character).
  • Redline slider max raised to 18,000; idle max raised to 2,000; displacement min dropped to 0.5 L.
  • Bikes get a cyan accent on the selector button and gauge needle.

v1.8.8 — Correct tach + better sounds

  • Rev counter fix: ticks now start at 0 RPM with clean round labels (1k, 2k, 3k…). Red zone aligned to where the limiter actually cuts (90% of max RPM).
  • Harmonic oscillator stack: the four oscillators moved to 1×, 2×, 3×, 4× the fundamental — a true additive harmonic stack.
  • Per-engine harmonic profiles: V8 crossplane gets strong 2nd/4th; V-twin gets strong 3rd; I4 SS sportbike gets bright 3rd/4th; boxers get strong 2nd; V12 is rich and complex.
  • RPM-scaled filters: high-revving engines get up to 65% more bandwidth through the low-pass chain.

v1.8.9 — Faster acceleration

  • Drive acceleration multiplier raised from 1.0 g → 2.5 g.
  • Aerodynamic drag coefficient halved (0.0002 → 0.0001).
  • Net effect: launches punch, mid-range pulls hard, and the car reaches gear-limited speeds noticeably faster.

Roadmap status

#Priority itemStatusVersion
1 Rev-limiter bounce Done v1.8.2
2 Downshift blip Done v1.8.3
3 Per-cylinder variation Done v1.8.4
4 Recent run history Done v1.8.5
5 PWA / offline mode Done v1.8.6

All five priorities from the original roadmap are complete. Three follow-on releases (bikes, correct tach, faster driving) have landed on top. The current build is v1.8.9.

Get Engine Sound lab

Download NowName your own price

Leave a comment

Log in with itch.io to leave a comment.