Skip to content

About

VCV Rack module bridging a Microsoft SideWinder Force Feedback 2 joystick and a modular patch in both directions: axes, hat and buttons as CV and gates, and CV driving the stick's force-feedback motors over USB HID PID.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

6 Commits

Folders and files

Repository files navigation

Sidewinder

A VCV Rack plugin that bridges a Microsoft SideWinder Force Feedback 2 joystick and a modular patch, in both directions:

  • the stick's axes, hat and buttons come out as CV and gates
  • CV goes back in and drives the stick's force-feedback motors
  • a live instrument display shows both halves at once

panel

The module

Stick to CV

Output Range Notes
X, Y ±5 V Y is positive when the stick is pushed away from you
TWIST ±5 V rudder twist
THROTTLE 0–10 V switchable to ±5 V, and invertible
HAT X, HAT Y ±5 V unit vector of the hat direction, 0 V when centred
RADIUS 0–10 V stick deflection from centre
ANGLE ±5 V bearing: 0 V forward, ±5 V dead astern
TRIGGER, B2–B8 0/10 V gates switchable to toggles

Axes pass through a circular dead zone and a one-pole smoother, both set from the context menu. The dead zone is radial rather than per-axis, so a stick resting slightly off centre cannot emit a diagonal.

CV to force

Each control is a knob plus a CV input; bipolar controls take ±5 V for full swing, unipolar ones 0–10 V.

Control Effect
FORCE X / FORCE Y constant force vector
SPRING centring stiffness
DAMPER resists velocity
FRICTION resists motion
INERTIA resists acceleration
SHAKE periodic effect depth
RATE periodic frequency, 1 V/oct on the CV input
WAVE sine, square, triangle, sawtooth up/down
GAIN device master gain
AUTO CENTRE hands centring back to the joystick's own spring
FORCE ON arms the actuators

Which way a commanded force actually pushes is set in the context menu under Force orientation: four quarter turns and an optional mirror, covering every axis-aligned mapping. The PID direction field is a polar angle whose zero and handedness are not identical on every device, so this is a setting rather than an assumption. To calibrate: turn FORCE X fully right, arm FORCE ON, and pick the option where the stick pushes right. A non-default orientation is shown under the force dial as R90, R180M and so on, so it is never a silent surprise.

By default the spring's neutral point follows FORCE X/Y, so a CV pair does not just push the stick, it places it: raise SPRING and the stick physically travels to the commanded position and holds there. Turn that off in the context menu if you want a fixed centre and a separate push.

Display

Left to right: the stick's position with a fading trail, its dead zone, the commanded force vector and the spring target it is being pulled toward; twist and throttle bars; a polar force dial with the spring ring and shake pulse; the hat rosette; the button grid; and a status block with the device name, the input and force update rates, effect slot usage and the effect types the hardware declares.

How it talks to the joystick

Input arrives through IOHIDManager, report-driven rather than polled.

Output does not use ForceFeedback.framework. That framework dispatches to a per-device force-feedback plugin looked up in the device's IOCFPlugInTypes, and macOS ships none for generic PID joysticks — the SideWinder's IORegistry entry advertises only IOHIDLib plugins, and a search of the whole IORegistry finds zero registrations of the force-feedback plugin type (F4545CE5-BF5B-11D6-A4BB-0003933E3E3E). FFIsForceFeedback() therefore cannot succeed for this device on a current macOS, whatever the API docs imply.

So the plugin speaks the USB HID Physical Interface Device protocol directly, which is what the framework would have done: Create New Effect and PID Block Load feature reports to allocate an effect block, then Set Effect plus the type-specific Set Constant Force / Set Condition / Set Periodic output reports, then Effect Operation to start it. Report layouts came from the SideWinder's own report descriptor and match the USB PID 1.0 class specification. See src/PidFF.cpp.

The device's PID state input report is read back as well, so the module can show whether the actuators are enabled, whether the actuator rail has power, and whether the hardware currently reports an effect playing.

Threading

One dedicated thread owns the CFRunLoop and every HID handle. The audio thread only ever reads and writes atomic scalars; it never locks, allocates or touches the OS. Force parameters are pushed to the hardware at 100 Hz with change detection, so a knob that is not moving costs no USB traffic.

Requirements and status

  • macOS. The device layer is implemented for macOS only. The module builds and loads on Windows and Linux but reports that no backend is compiled in; Windows would be served by DirectInput 8 and Linux by evdev's force-feedback ioctls.
  • The joystick's power supply must be connected. The SideWinder Force Feedback 2 runs its position sensing off USB but its motors off a separate 12 V supply. Without it the stick reports position perfectly and accepts every force command while doing nothing at all. The module says NO ACTUATOR POWER when the device reports that state, reading the bit from the device's own PID state report.
  • If macOS denies HID access, grant Rack Input Monitoring under System Settings → Privacy & Security.

What has been verified against the hardware

Measured on a real SideWinder Force Feedback 2 (045E:001B):

  • axes, hat and buttons read correctly, report-driven
  • the PID effect pool reads back as 1350 bytes with 15 simultaneous effects
  • effect allocation, parameter updates and start/stop all succeed with zero protocol errors, confirmed by the device's own Effect Playing status bit following the commands for constant force, spring and periodic effects
  • the device reports Actuators Enabled, Actuator Power and Safety Switch
  • force output works: the motors are driven by CV as described above
  • all three example patches load with every module and cable intact, checked by tools/check_examples.sh, which loads each one in a headless Rack and diffs the patch Rack saves back against the one it was given
  • the module loads, runs and unloads cleanly in a live engine with the device attached, with the device thread joining on plugin unload

Force direction is deliberately a setting rather than a measured constant, for the reason given under Force orientation above. On a unit where the mapping is in question, ./sw-probe --calibrate measures it closed-loop: it pushes the stick in eight directions and reads back where the stick's own position sensor says it went.

Examples

Six patches in examples/, each loadable as-is. They are generated by tools/gen_examples.py and checked by tools/check_examples.sh, which loads every one in a headless Rack and confirms nothing was dropped.

Starting points

01-joystick-voice.vcv - the stick played as an instrument. X goes through a quantizer into the VCO, Y opens the filter, the throttle sets resonance, and the trigger fires the envelope. The module's spring is switched on, which matters more than it sounds: the FF2 has no mechanical centring spring at all, so without the motors the stick simply stays wherever you leave it. Here the force feedback supplies the centring, and the pitch springs back when you let go.

02-cv-places-the-stick.vcv - two slow LFOs drive FORCE X and FORCE Y with the spring following them, so the stick physically walks a Lissajous figure around its whole travel while nobody touches it. The scope shows the position the stick actually reached, which lags and overshoots the commanded one.

03-haptic-sequencer.vcv - one CV row sets shake depth per step and another sets shake rate, so each step has its own texture in the hand while a third row plays the pitch. The joystick's trigger resets the sequencer.

Bidirectional and generative

These use the loop in both directions at once. The reason that is worth doing is that the stick is not a CV shaper: it is a real mass on real motors, with inertia, friction and a resonance of its own. Put it inside a modulation path and it contributes dynamics that no module in the rack will give you, and it never contributes them quite the same way twice.

04-physical-portamento.vcv - the glide between notes is a mass settling. The sequencer does not set pitch; it sets where the stick should be. The spring drags it there against its own inertia, and the pitch comes from where the stick actually is along the way, so overshoot becomes a blip past the note and damping becomes portamento time. Two slow random walks pull on the spring and the damper, so the articulation keeps changing while the notes do not. Turn DAMPER down for rubbery slides, up for clean steps.

05-self-oscillating-stick.vcv - the stick made into an oscillator, then played. Under spring and damping the stick is a second-order mechanical system, so its response to force already inverts above resonance; feeding its own position back in as force closes a loop that breaks into oscillation once the gain beats the damping. The stick starts moving on its own and that motion is the patch's modulation source. The lag on the X path lowers the pitch of the oscillation and a slow random walk on the spring keeps detuning it. Row 1 of the attenuverters is the loop gain and DAMPER is the brake: turn row 1 up until it sings, turn DAMPER up to calm it down. The defaults sit near the threshold deliberately, so how lively it is will depend on your particular stick.

06-generative-haptic-drone.vcv - the whole feedback path runs through the hardware. Two slow random walks pull the stick around its travel; where it ends up picks the pitch and the blend of two oscillators, and how far out it is opens the filter. The loop closes on the way back: the sound's own envelope drives the shake, the shake moves the stick, and the moved stick changes the sound. Nothing repeats and nothing is quantised in time. Grab the stick and you are inside the loop too.

Installing

Download the .vcvplugin for your platform from the releases and drop it into your Rack user folder's plugins-mac-arm64 (or the matching directory for your platform), then restart Rack. On macOS that folder is ~/Library/Application Support/Rack2/.

Building

make RACK_DIR=/path/to/Rack-SDK
make install RACK_DIR=/path/to/Rack-SDK

The panel and the widget layout header are generated together, so artwork and component positions cannot drift apart:

python3 -m venv .venv && .venv/bin/pip install fonttools
.venv/bin/python tools/gen_panel.py

The probe

sw-probe exercises the device layer with no Rack involved:

make probe
./sw-probe               # live axis, button and PID state readout
./sw-probe --force       # walk through each effect type, one at a time
./sw-probe --calibrate   # closed-loop force direction measurement

Licence

GPL-3.0-or-later. The panel is lettered with Roboto Condensed, under the SIL Open Font License; see tools/fonts/OFL.txt.

About

VCV Rack module bridging a Microsoft SideWinder Force Feedback 2 joystick and a modular patch in both directions: axes, hat and buttons as CV and gates, and CV driving the stick's force-feedback motors over USB HID PID.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages