Skip to content

How It Was Built Part 1

baddo edited this page Sep 28, 2026 · 1 revision

Part of How OpenShaker was built

Part 1: why OpenShaker exists

The setup

  • A ButtKicker PRO on USB-C. To Windows it is simply an audio output, "Speakers (ButtKicker PRO)", used in shared mode at 48 kHz.
  • HaptiConnect 2.7.0, the ButtKicker's own software, made by The Guitammer Company. It reads racing-game data and plays synthesized low-frequency sound to the shaker.
  • The games: Forza Motorsport, Forza Horizon 5 and 6, Assetto Corsa EVO (ACE) and BeamNG.drive. Trackmania came later.

HaptiConnect kept forgetting the shaker

September 10

Problem. Games often didn't "hook in": HaptiConnect wasn't listening on its network ports. In the maintainer's words: "half the time the software doesn't work, the game doesn't hook in".

What we found.

  • When HaptiConnect starts properly, its log gets one line per game within about a second, such as "BeamNG.drive bound to port 4444". That evening's log had only 12 lines, and not one of them was a "bound" line, so it could hear no game at all.
  • The first theory was a login or server check. Both of HaptiConnect's servers answered normally, and the latest failed starts showed no login error while something else was still missing (next point), so that was ruled out.
  • A good log has a line naming the device, "Speakers (2- ButtKicker PRO)", right after the saved device ID. The bad logs have the same saved ID but no name.
  • Across all 487 HaptiConnect logs on the PC: 208 have the device-name line, and 150 of those bound their ports within 5 seconds. Of the 279 without it, none bound within 5 seconds. Every start from late July to early September lacked it.
  • The device ID saved in HaptiConnect's settings no longer existed in Windows. The shaker had a new one.

Why. HaptiConnect remembers the shaker by a Windows device ID, not by its name, and Windows can give a USB device a new ID when it is re-plugged or re-detected. HaptiConnect then never finds its output, never starts its game plugins, and every game looks "not hooked in". A message inside HaptiConnect's program fits this picture: "Plugin system has been enabled while no spatial output is available".

Fix.

  • In HaptiConnect: pick the ButtKicker again under Spatial Configuration, restart it and check its log. We checked this: the device-name line came back, and "bound to port 4444" followed about a second later.
  • OpenShaker avoids the trap by design. It picks the shaker by name and ignores the "2-" that Windows adds after a re-plug. It looks the device up again on every start and never plays through another output. If the shaker is missing at startup, it tries again every 15 seconds. If the shaker goes away while playing, it tries every 3 seconds for the first minute, then every 15 seconds (see Unplug and replug).

The forgotten shaker came back

September 18

Problem. A week later, HaptiConnect's saved device was stale again. Anything replayed into it would have recorded silence.

What we found. A read-only check found the saved ID missing from Windows' device list again. HaptiConnect's own logs show that the shaker's ID had changed at least 14 times since December 2024. Even the "2-" in the name can change.

Fix. Every hardware session now starts by picking the ButtKicker again in HaptiConnect and checking its log. The rule: never replay into a HaptiConnect that hasn't bound its port.

Assetto Corsa EVO went silent

September 10

Problem. HaptiConnect has an Assetto Corsa EVO plugin, but it did nothing. In the maintainer's words: "One game I would like to work that doesn't right now is Assetto Corsa EVO".

What we found.

  • ACE doesn't send its data over the network. It shares it in named blocks of memory.
  • The text inside HaptiConnect's program shows that its ACE plugin opens acpmf_physics, acpmf_graphics and acpmf_static. Those are the names the older Assetto Corsa games use.
  • The installed game publishes acevo_pmf_physics, acevo_pmf_graphics and acevo_pmf_static instead. Community documentation says Early Access v0.6 also changed the layout of the data.
  • HaptiConnect added ACE in version 2.6.0 (March 2025), and no later release touched it. The SimHub build on the PC (December 2024) knew only the old names too.

Why. The game renamed its data, and HaptiConnect was never updated.

Fix. OpenShaker reads ACE's new memory directly, with nothing to set up in the game (Reading Assetto Corsa EVO's shared memory). The first live drive was that same evening. The maintainer's verdict: "felt pretty good". The proof that the diagnosis was right came later (Bringing HaptiConnect's ACE plugin back to life).

No Forza Horizon 6

September 10

Problem. There is no Forza Horizon 6 plugin, and borrowing the Horizon 5 one didn't work either. In the maintainer's words: "There still isn't a plugin for the new Forza Horizon 6", and "I tried piping in the FH6 bass shaker information into the FH5 ButtKicker plugin".

What we found.

  • HaptiConnect's list of game programs includes Forza Horizon 5 and Forza Motorsport, but not Horizon 6.
  • Its Forza plugins only open their ports (5301 for Horizon 5, 5305 for Motorsport) after they see their own game running. No Assetto Corsa or Horizon 6 connection appears in any of its logs.
  • Horizon 6 sends the same 324-byte packet as Horizon 5, so the data was fine. Nobody was listening, because Horizon 6 is a different program.
  • Later we confirmed it: a Horizon 6 lap, replayed into the Horizon 5 plugin while Horizon 5 sat at its menu, played normally (Horizon 6 without a plugin).

Fix. OpenShaker listens for every Forza game itself (port 5555), whichever one is running, and tells Horizon 5 from Horizon 6 by which program is open (Telling Horizon 5 from Horizon 6).

No more updates

Checked September 10

Problem. No Assetto Corsa EVO fix and no Horizon 6 plugin were on the way. In the maintainer's words: "I'm not sure if they're not supporting the software anymore or what".

What we found. HaptiConnect's official release notes listed 2.7.0 (June 2025, "Adds support for F1 2025") as the newest version when we checked in September 2026. The installed program matches it.

Fix. Build a replacement, and keep HaptiConnect only as the measuring stick. The README and HOW_IT_WAS_TUNED.md both say so.

Timing that wandered

Raised and measured September 27

Problem. While driving with HaptiConnect, the maintainer found the delay from game to shaker sometimes random on Forza Horizon 5, and drifting over a session on Forza Motorsport: "sometimes the latency would be random on FH5 and Motorsport would drift over time." This was about HaptiConnect: "Our performance is fine."

What we found. We measured it from the recordings we already had, instead of taking it on faith.

  • The method: for every clean gear change, time the gap from the replayed data reporting the shift to HaptiConnect's gear-change thump.
  • Most recordings: a median of about 75-92 ms, with no trend.
  • One Horizon 5 replay jumped from shift to shift: 247, 249, 73, 235, 156 and 240 ms.
  • On two Motorsport replays, single gear changes arrived 200-340 ms late.
  • HaptiConnect looks at the game data only about 12 times a second. That can explain scatter of up to about 85 ms, but probably not jumps of 150-250 ms.
  • Each replay lasts only a few minutes, so a slow drift over a whole session could not be shown.

Fix. The public docs give the random delays as measured and the drift as the maintainer's experience. For OpenShaker they say only what the code guarantees: it renders fixed 10 ms blocks from the newest data, with a constant 1.3 ms look-ahead in its limiter. Its live end-to-end delay has not been measured (what is still open).

How HaptiConnect works, seen from outside

September 10

Problem. Could Assetto Corsa EVO's data be fed into HaptiConnect instead? And how does HaptiConnect turn game data into vibration at all? In the maintainer's words: "can we capture the ACE output then feed it into the HaptiConnect software".

What we found.

  • The program is a Qt app with the FMOD audio engine.
  • Each game has a thin adapter: network listeners for Forza and BeamNG, memory readers for Assetto Corsa. All the adapters feed one shared set of effects: engine rpm, gear shift, acceleration, collision, suspension, wheel lock, wheel slip, ABS, rumble strip, shift indicator and road texture.
  • Per-game profile files choose which effects each game gets (6 for BeamNG, 8 for ACE, 9 for Forza Horizon 5) and how strong each one is.
  • The games send only numbers, never sound. HaptiConnect synthesizes everything and plays it to the shaker, which is just an audio device.

Why it mattered. Both ends are open. Anyone can send the input packets, and Windows can record the output digitally. So HaptiConnect could be treated as a black box: control the input, record the output and work out the rules, all without opening its code. That made a replacement practical.

Patch it or replace it?

September 10 to 12

Problem. The maintainer's first idea was a patch: pass Assetto Corsa EVO's (and Horizon 6's) data to HaptiConnect disguised as BeamNG, whose plugin "works great". In the maintainer's words: "Would it be possible to intercept the ACE bass shaker output to the ButtKicker program". Minutes later came the bigger question: "Build our own application that acts as HaptiConnect."

What we found (the reasoning).

  • 2.7.0 was the last release, so no fix was coming.
  • A translator would still suffer from the forgotten shaker, and it would play ACE and Horizon 6 through BeamNG's six-effect preset instead of a Forza or ACE one.
  • SimHub was already on the PC as a no-code fallback, but its build predated ACE and Horizon 6 support.
  • The problems (the fixed memory names, the game detection, the stored device ID) all live inside a closed program that can't be fixed from outside.

Fix.

  • The first build did both: a standalone engine that plays straight to the shaker, plus a translator mode that fed HaptiConnect's BeamNG plugin. The standalone engine quickly became the product.
  • Claude offered a small helper that would republish ACE's data under the old names for HaptiConnect. The maintainer declined: "We don't need to build anything else. If our app supports it that is enough."
  • On September 12: "I don't need to be able to forward to HaptiConnect". Forwarding was removed from the app, and the packet-building code now lives only in the calibration tools.
  • Asked "Do I need to have HaptiConnect installed any more?", Claude answered no for everyday use: it is only needed to record new comparisons.

HaptiConnect becomes the measuring stick

September 10, sharpened September 18

Problem. Replacing HaptiConnect must not cost the feel the maintainer liked, and after the first side-by-side, HaptiConnect felt better. In the maintainer's words: "I just want the spectrograms to match, that way we know it's the same".

What we found. There is no specification of what HaptiConnect plays, so its recorded output is the only ground truth.

Fix.

  • September 10, getting out of the chair: "do calibration based off the comparison of the outputs not my feedback", and "I'd like it to be as close as HaptiConnect." Those sentences were copied word for word into every later handoff note as the project's rule.
  • September 18 went further: exactly the same, "including loudness and intensity". In the maintainer's words: "I want to mimic the feel of the output from the HaptiConnect driver". This undid an earlier choice to play BeamNG quieter (BeamNG's loudness changed three times).
  • The method (log a drive, replay the same data into HaptiConnect and record it, render OpenShaker from the same data, fit, and check on drives left out of the fitting) is the subject of HOW_IT_WAS_TUNED.md.


<- How it was built | Contents | Part 2: how we worked ->

Clone this wiki locally