Skip to content

Repository files navigation

Orion

A mouse click sound player, built so that a burst at twenty clicks per second stays indistinguishable from a real hand on a real mouse.

#include"Orion.h"
orion::Player player;
player.start({}); // hook installed, fallback synth loaded
player.loadBank("gpro"); // or a recorded bank
player.notifyClick(orion::Button::left); // from an autoclicker

Why this is not just "play a wav"

Most implementations play one sample per click with a bit of gain jitter. It sounds fake, and the reasons are nameable:

  • The release is frozen. A take that contains the whole cycle bakes in a constant press to release delay. At twenty clicks per second that delay is wrong; on a held button it is absurd. Here they are two separate events, and the scheduled release lands on the exact sample.
  • The waveform is identical every time. What gives a burst away is not that it is a sample, it is the spectrally identical fingerprint from one click to the next. Gain jitter does not hide that. Here: anti repetition round robin over a three deep history, pitch variation through Hermite interpolation, gain variation, and a random start offset of a few dozen samples that decorrelates the phase and breaks the comb.
  • Nothing ties the strike to the rate. A click chained fast is louder and sharper than a settled one. Velocity comes from the interval since the last click, and drives gain and brightness.
  • Butterfly is not modelled. Two fingers alternate, and they do not hit the same spot on the shell. What you hear as butterfly is two timbres alternating, not disorder. With slip, because perfect alternation sounds mechanical.

Effects chain

The output bus runs through a fixed order chain, the one you would follow by hand: shape the transient, tint it, saturate, compress once the timbre is settled, place it in space, and leave through a ceiling.

stagewhat it does
transientattack and tail separately. On clicks this is the most effective tool in the chain
EQhigh pass, low shelf, bell, high shelf. A click lives between 2 and 10 kHz
drivenormalised tanh, with a mix control
compressorsoft knee, linked detection across channels, auto makeup, gain reduction metering
roomearly reflections, eight irregular taps, damped
echofiltered delay in the loop, feedback, ping pong
outputmid/side width, gain, ceiling

Two choices are worth calling out.

Early reflections rather than a reverb. A real click reaches your ears with the room's reflections in the first few tens of milliseconds, and that is exactly what a close miked sample is missing. A reverb tail would only push it further away.

The transient shaper rather than the compressor for punch. On a twenty millisecond event a compressor is always late: by the time its detector reacts, the transient is already out.

Everything is neutral by default. At rest the chain does nothing but bound the bus, and it is the chain that does it: the mixer sums without limiting, so the effects see the real signal.

Layout

targetwhatplatform
orionbank, selector, voice, mixer, synth. No hardware callsall
orion_inputlow level mouse hookuseful on Windows, empty elsewhere
orion_deviceminiaudio output, and the Player facadeall
clickbankslicing, checking and packing banksall
orion_demolistening and tuning benchall

orion never touches the sound card: everything in it renders and verifies offline, and the tests link it alone. That is what lets verification run with no screen and no audio device.

Nothing in the render path allocates, locks or calls the system. Everything is sized at load time, takes are decoded and converted to the device rate once and for all, and the event queue refuses rather than waits: blocking in the callback would cost a hole in the sound, which is worse than a dropped click.

Build

cmake -B build
cmake --build build --config Release
ctest --test-dir build -C Release --output-on-failure

The only dependencies are vendored or fetched on demand: miniaudio as a single header in external/, and imgui plus glfw for the demo only, through FetchContent. orion itself has no dependency at all, WAV decoder included.

Options: ORION_BUILD_TESTS, ORION_BUILD_TOOLS, ORION_BUILD_DEMO.

Banks

One bank ships, banks/op1v2: an Endgame Gear OP1 v2 recorded here with a close mic. Normal on both buttons, jitter on the left, no butterfly. It is what both presets name and what the worked examples in the docs run against, so the thing you hear out of the box is a real mouse and not the fallback.

The library is never silent either way: with no bank at all it plays a synthesised profile. That is a reference point, not a replacement, and the a/b toggle in the demo exists to make the gap obvious.

To build your own, record your own mouse. It takes twenty minutes and beats any third party source: docs/RECORDING.md walks through the session, docs/BANKS.md through what happens to the material afterwards.

clickbank slice session.wav -o banks/gpro --technique normal
clickbank check banks/gpro

Any other bank stays local. banks/* is excluded apart from op1v2, because a bank sliced out of someone else's recording is not ours to hand on.

Presets

presets/ holds two, both for the same mouse, and they are there as worked examples rather than as something to load blindly:

presetwhat it is for
op1v2-canon.jsonflat. No effects, no listening geometry. This is the one to render against the original recording with clickbank match, because it judges the bank and nothing else
op1v2-bureau2.jsonthe full thing. EQ, drive, echo, and the listening geometry of a mouse sitting to the right on a desk. This is what a tuned setup looks like

A preset names the bank it was tuned on. Loaded against a different bank, the numbers still apply but they no longer mean what they meant: the variation the engine adds is meant to complete what the bank already carries, not to stack on top of it.

clickbank inspect presets/op1v2-bureau2.json resolves one all the way down and says what the engine will actually do with it, clamps and unchecked boxes included.

Integrating

src/Orion.h is the only header to include.

For a click emitter that notifies directly, set EngineConfig::injected to ignore: the emitter's SendInput comes back through the hook flagged as injected, and would sound twice.

ClickEvent::holdSec carries three cases:

  • positive: the engine schedules the release at that deadline
  • zero: no automatic release, the producer will send its own. This is the hook's case, and the default
  • negative: the engine draws a plausible duration for the current technique

Contributing

Conventions are in docs/CONVENTIONS.md. They are short, and following the neighbouring file is usually enough. Before pushing:

cmake --build build --config Release
ctest --test-dir build -C Release --output-on-failure

Licence

MIT, see LICENSE.

miniaudio (external/miniaudio) is public domain or MIT-0, your choice. imgui and glfw are fetched for the demo only.

About

Humanized click audio player with a built in effects chain. C++17, lock free audio path, miniaudio (WASAPI/ALSA), Dear ImGui tuning bench, and a home made WAV / slicer / FFT measurement toolchain.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages