Welcome to the new Whirlwind Engine, powered by EX.

Introduction

If you are here, congrats on being selected as a power-user with beta access to our products and software. We're developing the new engine rapidly, which means lots of regular updates as features coalesce. This documentation will serve as a portal for us to describe how things have changed, what we are working on, and how they can help you design more compelling experiences by employing simple web technologies to create presets and visualizations. Below, we'll briefly describe what is new.

All of our products will be automatically discovered by the engine and made available to you on the 'devices' tab. For more detailed information about the connected device, you can press the settings (gear) button to expose a panel with additional product information. Currently, we only connect to and detect Whirlwind products, but we are working with external partners to expand the supported devices in the future.
Since there are so many available lighting systems, we've designed and patented a peripheral lighting rendering flow that uses HTML5 Canvas + Javascript for controlling lighting on all peripherals in unison. There will be more details in this document as you continue to read.
We allow two types of effects, but under the hood they are created and, for the most part, behave identically. Media effects grant direct access to EX analytics such as audio RMS level, frequency information, video color and information, and much much more. Idle effects are drawn on all peripherals when the user is not or does not want to engage a Media Effect (such as games, videos or music).

Download

You can download the engine installer here:

Requirements & Drivers

In order for our launcher and engine to function, you'll need to make sure you've got 64-bit visual studio runtimes installed. Those can be found here:

64-bit Microsoft Runtimes

In rare cases, you may also need x86 runtimes if they are not preinstalled on your system. Those can be found here:

x86 Microsoft Runtimes

If you connect a lightstrip and it isn't showing up in the devices pane of the engine, you may need serial drivers. Those can be found here:

Lightstrip Serial Drivers

If you connect a Vortx and it isn't showing up in the devices pane of the engine, you may need drivers. Those can be found here:

Vortx Serial Drivers

If you are having any other issues, please contact support and they will make sure you get you taken care of. Additionally, you can join our discord server at the address below.

Release Notes

We are working hard to make sure that both our lighting engine and our EX Analytics core meets the demanding requirements we have set for gaming. This includes support for 2K content at framerates exceeding 100fps. If you have feedback that you'd like to communicate to us, please use the beta channel on Discord to propose any questions or concerns. You can join the engine beta channel here:

Join Beta Channel

If this link doesn't work for you, please contact support and they will make sure you get an invite code. We've limited this initial pilot to a fixed number of invites.

Engine Overview

The engine presents a new way to manage peripherals and effects. Products are auto-detected, and present on the devices tab when discovered. For each device, you can pick a position and scale to control which part of the active effect is rendered on the internal LEDs.

Idle vs Media Effects

An important principle to understand, is that there are two types of effects. Idle effects, which are rendered when there is no audio playback detected on the system, and media effects, which are meant for when audio and video content is actively being played back on the system. You can configure the engine to automatically switch which visual effect is rendered on all of your devices based on what you are currently doing.

Idle effects do not use audio or video or information, and are generally more traditional (although highly customizable). Rainbows, Hue Cycle, and many things are possible using the idle effect infrastructure. Media effects have deeper access to our analytics system, and can sample portions of the screen to create incredibly deep ambient and intelligent effects based on instantaneous audio and video parameters.

Selecting a Idle Effect

On the effects tab, you can select your primary idle effect with the appropriately named dropdown.

Selecting a Media Effect

You can also choose a media effect to render during video by choosing on the effects tab. The best way to view these effects is to have video and audio playing while choosing which look you prefer.

Vortx

Vortx creates wind and heat effects synchronized with the action on screen. To do this, Vortx analyzes the audio and video of your game to trigger bursts of air effects.

For proper operation of Vortx, you'll need to ensure that media effects are enabled (they are by default) and that audio and video capture are functioning correctly on your system.

Front panel knob.

On the front of Vortx, you can control the sensitivity of the processing, as a volume knob for 'air level' of sorts. As you reduce this knob, Vortx becomes more picky about content and outputs less air.

The Vortx main knob is also a button. Pressing the knob cycles through three values for Vortx:

  • Air Intensity Default
  • Heat Intensity
  • Disconnected Hue

You can press the knob to cycle between these three values and turn to set to your liking. The values you choose are visible both on the LED ring of the knob and within the settings pane of the engine. After 5 seconds of not adjust the knob, it will switch back to its default 'Air Intensity' function. Since this is most often adjusted, you need not press the knob to set this value; simply turn to adjust to your liking.

Element

Element is a mechanical keyboard designed for ambient immersion, insane particle effects, and hardcore performance.

Additionally, we support a growing list of intelligent game extensions to create visual effects across your entire rig when you take damage, increase health, or see fire in the frame. We are actively developing this infrastructure and it has been a crucial part of forward development for our team.

Scan Rate 1300hz - 1400hz
Lighting High-emission per-key RGB [Intelligent]
Debounce None [Zero-Bounce Technology]
Rollover N-Key
Body Material Aerospace-Grade Brushed Aluminum
Actuation Distance 1.7mm
Actuation Force 50G
Contacts 24-carat Gold
Key Configuration Full 104-Key ANSI
Available Keytypes Linear, Clicky, Mixed

Ex Key

You can use the function + pause/break key to show or hide the Engine. Pressing it once brings the Whirlwind Engine to the foreground, and pressing it a second time or while already in the foreground will minimize the Engine to the task tray.

Fn+Media Controls

Element incorporates standard brightness and media controls.

  • Brightness Up
  • Brightness Down
  • Track Prev
  • Track Play/Pause
  • Track Next
  • Vol Mute
  • Vol Down
  • Vol Up

Effect Development & Locations

All installed effects are parsed on startup and made available to the lighting subsystems. If you'd like to create your own effect, you'll need to place it in the 'Effects/idle' or 'Effects/media' subdirectory, and give it a unique name with an .html extension. After that, you'll need to restart the engine for that effect to be picked up and available for selection.

  • Effects Local Effect Location
    • Static (Idle) Installed Idle Effects
      • Rainbow.html Idle Script
      • Hue Cycle.html Idle Script
      • Terminal.html Idle Script
    • Dynamic (Media) Installed Media Effects
      • Call of Duty: Warzone.html Media Script
      • Fortnite.html Media Script
      • Natural.html Media Script
  • WhirlwindEngine.exe Engine Executable

Effects in the "Dynamic" directory will automatically inject the variables detailed in the API reference documentation. These analysis variables are not injected or supported in effects contained within the "Static" directory. If media effects are enabled on the effects tab in the UI, the system will auto-switch to the selected media effect only when audio/video content is detected. If audio data is silence, the system will fallback to the selected idle effect.

Content & Game Extensions

Content extensions allow us to drive extra features for games, such as health, shields or really any other metered item on the hud. Tagged extensions are automatically enabled when relevant content is being consumed on the target machine.

We are currently expanding our internal analysis system to support an increasing number of titles and context awareness methods. (Text, meters, etc) The system's extensions are driven largely by a JSON document that contains some helpful positional information related to metering, and tag associations for the relevant titles.

If this is interesting to you, or you'd like to see a specific game supported with internal extensions for health/damage or an arbitrary metering condition, please email support to make a request or reach out to the private Discord alpha group.

Effect Overview

Our engine renders effects in realtime, similar to the way a browser renders websites you visit. A simple hue cycle example follows.

                              
    <head>
    <meta description="Stock hue cycle."/>
    <meta publisher="WhirlwindFX" />
    <meta property="speed" label="Cycle Speed" type="number" min="1" max="10" default="2">
	</head>
                              

    <body style="margin: 0; padding: 0;">
  	<canvas id="exCanvas" width="320" height="200"></canvas>
	</body>
                              

    <script>
    // Get the canvas element from the DOM
    var c = document.getElementById("exCanvas");
    var ctx = c.getContext("2d");  
    var width = 320;
    var height = 200;
    var hue = 0;

    function loop() 
    {
      ctx.fillStyle = 'hsl('+ hue + ', 100%, 50%)';
      ctx.fillRect(0, 0, width , height);
      hue+=(speed / 4);
      if (hue > 360) { hue = hue % 360; }
    }

  	setInterval(loop, 20);
	</script>                          
                              

    <head>
    <meta description="Stock hue cycle."/>
    <meta publisher="WhirlwindFX" />
    <meta property="speed" label="Cycle Speed" type="number" min="1" max="10" default="2">
	</head>
    
    <body style="margin: 0; padding: 0;">
  	<canvas id="exCanvas" width="320" height="200"></canvas>
	</body>
    
    <script>
    // Get the canvas element from the DOM
    var c = document.getElementById("exCanvas");
    var ctx = c.getContext("2d");  
    var width = 320;
    var height = 200;
    var hue = 0;

    function loop() 
    {
      ctx.fillStyle = 'hsl('+ hue + ', 100%, 50%)';
      ctx.fillRect(0, 0, width , height);
      hue+=(speed / 4);
      if (hue > 360) { hue = hue % 360; }
    }

  	setInterval(loop, 20);
	</script>                          
                              

The Effect Plane

The result of a rendered effect is a 320px x 200px frame that is used to drive all connected peripherals. This frame, we refer to as the 'effect plane' and is visible on the 'Effect' tab in the engine. You can position peripherals on this plane, and in common terms, they will take the colors underneath them based on a selected scale and x/y position. If you select a device on the 'Devices' tab of the Whirlwind Engine, you'll see a position button in the bottom-right hand corner (a set of arrows). This allows you to place and move any device to a desired geospacial position on the plane.

This means that if you build a left to right 'sweep' on the effects plane, and place your devices in their repective areas in the software, the sweep will move left to right through all supported peripherals across your desk.

API Reference

User Customization Controls

Controls allow the end user of the script to adjust settings of the script via the engine UI. They are defined using HTML meta tags placed inside the <head> tag of the script.

Controls are described using meta tag attributes. The following attributes are valid for all types of controls:

Attribute Value
property The name of the variable you wish to assign to the control.
label The label to display to the user.
type The type of control. Currently, the following options are valid: boolean, number, hue, color
default The default value for the control.

Number Slider

The number control allows the user to select the value of a number using a slider.

<property="myNum" label="Number Control" type="number" default="5" min="0" max="10"> Number Slider control

This control supports the following attributes:

Attribute Value
property The name of the variable you wish to assign to the slider.
label The label to display to the user.
type number
default The default value for the slider.
min The minimum selectable value for the slider. This attribute supports negative values.
max The maximum selectable value for the slider.

Boolean Switch

The boolean control allows the user to select the value of a boolean variable using a toggle control.

<meta property="myBool" label="Boolean Control" type="boolean" default="0"/> boolean control

This control supports the following attributes:

Attribute Value
property The name of the variable you wish to assign to the toggle control.
label The label to display to the user.
type boolean
default The default value for the toggle. Set this to 1 to make "on" the default state.

Hue Slider

The hue picker control allows the user to select the hue component of a color with a slider control

<meta property="myHuePicker" label="Hue Picker Control" type="hue" default="120" min="0" max="360"> Hue slider control

This control supports the following attributes:

Attribute Value
property The name of the variable you wish to assign to the hue control.
label The label to display to the user.
type hue
default The default value for the hue slider.
min The minimum selectable hue value. This value must be between 0 and 359
max The maximum selectable hue value. This value must be between 1 and 360

Colorpicker

The colorpicker control allows the user to select a color using a hue wheel, saturation slider, and luminance slider.

<meta property="myColorpicker1" label="Colorpicker Control" type="color" default="#009bde" min="0" max="360"/> Color picker control

This control supports the following attributes:

Attribute Value
property The name of the variable you wish to assign to the colorpicker control.
label The label to display to the user.
type hue
default The default value for the colorpicker. This must be specified as a hex value in the form #RRGGBB
min The minimum selectable hue value. This value must be between 0 and 359
max The maximum selectable hue value. This value must be between 1 and 360

Meters

Meters enable the developer to leverage sophisticated vision processing techniques. They are also defined using HTML meta tags placed inside the <head> tag of the script.

Meters are described using meta tag attributes. The following attributes are valid for all types of meters:

Attribute Value
meter The name of the variable you wish to assign to the meter. These can be accesed with engine.vision.meter where meter is the name specified here.
type The type of meter. Currently, the following options are valid: area, linear, ocr_numeric, ocr_textmatch
x The x position of the meter, in normalized coordinate form.
y The x position of the meter, in normalized coordinate form.

Normalized Coordinates

Normalized coordinates are how screen coordinates must be communicated to the engine. This ensures that meters work at all 16:9 screen resolutions.

To calculate a normailzed coordinate, simply divide the value by the width or height of your monitor in pixels. For example, to get the normalized x coordinate of a point at x=1800, y=650 on a 3840 x 2160 monitor, you would divide 1800 by 3840 to get a value of 0.46875. This is the normalized x-coordinate. To find the normalized y coordinate, just do the same process using the screen height (in this example, 2160) and y coordinate.

Linear Meter

The linear meter allows you to analyze a linear area of the screen and get the percentage, read left-to-right, of the area that matches a certain color or range of colors.

<meta meter="myLinear" type="linear" x=".1729" y=".9740" width=".0390" h="0-360" s="0-10" l="90-100"> ...

This meter supports the following attributes:

Attribute Value
meter The name of the static variable you wish to assign to the meter. These can be accesed with engine.vision.meter where meter is the name specified here.
type linear
x The x position of the meter, in normalized coordinate form.
y The x position of the meter, in normalized coordinate form.
width The width of the region, in normalized form.
h The hue or hue range that should be considered a match.
s The saturation value or saturation range that should be considered a match.
l The luminance value or luminance range that should be considered a match.

Area Meter

The area meter allows you to analyze a rectangular area of the screen and get the percentage of the area that matches a certain color or range of colors.

<meta meter="myArea" type="area" x=".1729" y=".9740" width=".0390" height=".02" h="0-360" s="0-10" l="90-100">

This meter supports the following attributes:

Attribute Value
meter The name of the variable you wish to assign to the meter. These can be accesed with engine.vision.meter where meter is the name specified here.
type area
x The x position of the meter, in normalized coordinate form.
y The x position of the meter, in normalized coordinate form.
width The width of the region, in normalized form.
height The height of the region, in normalized form.
h The hue or hue range that should be considered a match.
s The saturation value or saturation range that should be considered a match.
l The luminance value or luminance range that should be considered a match.

OCR | Get Number

The numeric OCR meter uses optical character recogniton to try and get an integer value from an area on the screen containing a number.

<meta meter="myNumOCR" type="ocr_numeric" x=".1729" y=".9740" width=".0390" height=".02" confidence="70">

This meter supports the following attributes:

Attribute Value
meter The name of the variable you wish to assign to the meter. These can be accesed with engine.vision.meter where meter is the name specified here.
type ocr_numeric
x The x position of the meter, in normalized coordinate form.
y The x position of the meter, in normalized coordinate form.
width The width of the region, in normalized form.
height The height of the region, in normalized form.
confidence A value that defines what is considered a match. This attribute defaults to 70 if it is not specified.

OCR | Match Text

The text OCR meter uses optical character recogniton to search for a specified string in the given region.

<meta meter="myTextOCR" type="ocr_textmatch" x=".1729" y=".9740" width=".0390" height=".02" string="my string" confidence="70">

Along with the global meter attributes, this meter supports the following additional attributes:

Attribute Value
meter The name of the variable you wish to assign to the meter. These can be accesed with engine.vision.meter where meter is the name specified here.
type ocr_textmatch
x The x position of the meter, in normalized coordinate form.
y The x position of the meter, in normalized coordinate form.
width The width of the region, in normalized form.
height The height of the region, in normalized form.
string The string to search for.
confidence A value that defines what is considered a match. This attribute defaults to 70 if it is not specified.

Content Properties

Current Audio Level

engine.audio.level returns the current audio level in decibels.

Current Audio Width

engine.audio.width returns the percieved stereo width of incoming audio as a ratio from 0->1.

Current Audio Density

engine.audio.density returns the frequency density of incoming audio as a ratio from 0->1. Whitenoise being higher values near 1.0, test tones being values near 0.

Current Audio Spectrum

engine.audio.freq returns an array with 200 elements, each representing the current level of that frequency range.

Screen Zone Hue

engine.zone.hue returns an array with 560 elements, each corresponding to the hue value of a point on the screen. These values are sampled from a 28 x 20 grid.

Screen Zone Saturation

engine.zone.saturation returns an array with 560 elements, each corresponding to the saturation value of a point on the screen. These values are sampled from a 28 x 20 grid.

Screen Zone Lightness

engine.zone.lightness returns an array with 560 elements, each corresponding to the lightness value of a point on the screen. These values are sampled from a 28 x 20 grid.

Helpful Snippets

Meter Helper

The meter class can be used to easily track changes in metered data or other kinds of data that change frequently. To initialize a new meter, provide the number of values you'd like to compare each time, along with a callback to execute when the meter changes.

Method Description
Meter(strength, callback)

The constructor accepts both a strength and a callback.

strength - the number of times an identical value should be present in order for the meter value to be considered 'stable' enough to be processed. Higher values result in more accuracy, lower values result in less latency.

callback - a function to be called when the meter value is stable and has changed.

Meter.setValue(rawValue) You should call setValue(rawValue) every frame, or any time new raw data for this meter is available.
Property Description
Meter.value The current value of the meter.
Meter.increased A boolean that indicates whether or not our value has increased this update
Meter.decreased A boolean that indicates whether or not our value has decreased this update
Meter.diff The absolute value of the change the meter has experienced this update.
                                                     
<script> 
// Snippet - Meter(strength, callback) 1.0.1
function Meter(e,s){this.size=e,this.value=0,this.diff=0,this.increased=!1,this.decreased=!1;var t=[];this.setValue=function(e){t.push(e),t.length>this.size&&t.shift();for(var i=0;it[0],this.value=t[0],s())}}
</script>
                             

  //Initialize a new meter
  var healthMeter = new Meter(3, onHealthChanged);
  
  
  function update() {  
    //This updates the value - we need to do this every frame.
    myMeter.setValue(engine.vision.health);
    
    // Request next frame.
    window.requestAnimationFrame(update);    
  }
  
  
  function onHealthChanged() {
        
  	if(healthMeter.value === 20){
    	//do something if health is exactly 20.
    }
        
    if(healthMeter.increased) {
       //do something if the meter has increased.
    }
    
    if(healthMeter.decreased) {
       //do something if the meter has decreased.
    }
    
    if(healthMeter.diff > 5) {
       //do something if health has changed by more than 5.
       //note: diff is an absolute value
    }

  }    
                             
                                                     
  function Meter(count, callback) {
    this.size = count;
    this.value = 0;
    this.diff = 0;
    this.increased = false;
    this.decreased = false;
    var values = [];

    this.setValue = function (updatedValue) {
      // Add and shift.
      values.push(updatedValue);
      if (values.length > this.size) {
        values.shift();
      }

      // Exit early if we've got a long-term match.
      for (var i = 0; i < values.length - 1; i++) {
        if (values[i] !== values[i + 1]) return;
      }

      // We got here, so weve got a matching value collection.
      if (this.value !== values[0]) {
        //var fromZero = this.value === 0;
        //var toZero = values[0] === 0;
        this.diff = Math.abs(this.value - values[0]);
        this.increased = this.value < values[0];
        this.decreased = this.value > values[0];
        this.value = values[0];
        callback();
      }
    };
  }
                             

State Helper

The state manager class is a state stack that can be used to manage various effect states. Below is an annotated example of how to use the state manager.

Method Description
StateHandler()

The constructor requires no arguments.

StateHandler.push(new State()) You can push a new state onto the state stack using push, and processing will begin immediately.
StateHandler.pop() Pops the current state off the state stack, and begins processing the previous state on the state stack.

The only requirement for an object pushed onto the state stack, is that it must implement Process().


<script> 
// Snippet - StateHandler() 1.0.1
function StateHandler(){function t(){n=0<u.length?u[u.length-1]:null}var u=[],n=null;this.Push=function(n){u.push(n),t()},this.Pop=function(){u.pop(),t()},this.Process=function(){null!=n&&n.Process()}}
</script>                              
   
                              
  // Example state that runs for two seconds, and then pops
  // itself off the state stack when complete.  This state requires you
  // to pass the global StateHandler() object into the constructor
  function TwoSecondState(manager) 
  {
    this.handler = manager;
    this.start = new Date().getTime();
    this.duration = 2000;
  
    // Every state must implement Process()
    this.Process = function () 
    {
      
      this.elapsed = new Date().getTime() - this.start;      
      if (this.elapsed > this.duration) {
        handler.Pop(); // Pops this state off the stack!
      }
      
      this.Draw();
    };
  
    this.Draw = function () {
        
	  // Drawing logic goes here
      
    };    
  }  
                             

  // Declare new statemanager
  var stateMgr = new StateHandler();   
        

  //Start logic after engine is ready
  function onEngineReady() {
  
    // Grab canvas and rendering context.
    canvas = document.getElementById("exCanvas");
    ctx = canvas.getContext("2d");

    // Push starting state onto the state stack.
    stateMgr.Push(new DrawScreenColors());
    
    // Start updates *after* our engine is accessible and ready.
    window.requestAnimationFrame(update);
  }
  
  
  // Update is called every frame.
  function update() {
        
        // Conditionally push new state to manager.
  		if(bShouldSwitchState) {
     		stateMgr.Push(DrawMyTwoSecondEffect());
  		}
        
        //Service state manager
        stateMgr.Process();
        
        // Request next frame.
        window.requestAnimationFrame(update);
   }
     
            
  // Example state that runs perpetually, and draws
  // screencolors to the canvas.
  function DrawScreenColors() {
    this.Process = function () {
      let lightness = new Int8Array(engine.zone.lightness);
      let sat = new Int8Array(engine.zone.saturation);
      let hue = new Int16Array(engine.zone.hue);

      for (var iZone = 0; iZone < 560; iZone++) {
        ctx.fillStyle =
          "hsla(" +
          hue[iZone] +
          "," +
          sat[iZone] +
          "%," +
          lightness[iZone] +
          "%, " +
          keyScrenBrightness * 0.01 +
          ")";

        var iRow = Math.floor(iZone / 28);
        var iCol = iZone % 28;
        var iWidth = 320 / 28;
        var iHeight = 200 / 20;
        var iZx = iCol * iWidth;
        var iZy = iRow * iHeight;

        ctx.fillRect(iZx, iZy, iWidth, iHeight);
      }
    };
  }
        
        
  // Example state that runs for two seconds, and then pops
  // itself off the state stack when complete.
  function DrawMyTwoSecondEffect() {
    this.start = new Date().getTime();
    this.duration = 2000;
  
    this.Process = function () {
      this.elapsed = new Date().getTime() - this.start;
      
      if (this.elapsed > this.duration) {
        stateMgr.Pop();
      }
      
      this.Draw();
    };
  
    this.Draw = function () {
        
	  // Drawing logic goes here
      
    };
    
  }  
                             

  function StateHandler() {
    var stack = [];
    var state = null;

    var updateState = function () {
      if (stack.length > 0) {
        state = stack[stack.length - 1];
      } else {
        state = null;
      }
    };

    this.Push = function (newState) {
      stack.push(newState);
      updateState();
    };

    this.Pop = function () {
      stack.pop();
      updateState();
    };

    this.Process = function () {
      if (state != null) {
        state.Process();
      }
    };
  }