Music-reactive worlds with Pulse

Since SDK 0.2.1

Pulse listens to the music of your world and turns it into numbers your shaders can read every frame: the level of four bands (bass to treble) and their last 1.4 seconds, the spectrum, smoothed levels, a musical time that runs faster on loud beats, and four colors taken from the notes that dominate. The package also has a tablet that players hold to tune all of it during a party.

It is the Unity package NodeWorld Pulse (com.nodeworld.pulse), under the MIT licence. It needs NodeWorld SDK Worlds, which it brings.

Add the package

  1. Add the NodeWorld registry to your project, as for the SDK (install the SDK).
  2. Open Window, Package Manager, choose My Registries, select NodeWorld Pulse, then Install.
  3. In your world's NW World Config, add the seven scripts of Packages/com.nodeworld.pulse/Runtime/Flux/ to Flux Sharp Sources (in the Project window, they are under Packages, NodeWorld Pulse, Runtime, Flux): PulseAudio, PulseTablet, PulseHandle, PulseSharedSlider, PulseSharedToggle, PulseThemeColors, PulseLabels.

Connect the music

  1. Drag the Pulse prefab (Packages, NodeWorld Pulse, Runtime, Prefabs) into your scene. It holds the engine: an FL# Script component with the class Pulse.PulseAudio.
  2. In its field source, put the Audio Source that plays your music: a speaker of the scene, or the sound output of a video screen.

Pulse reads what the source plays, whatever its volume: a player who turns the music down still sees the lights react. When the source is silent or stopped, every value falls back to 0.

One engine per world is enough: every shader of the scene reads the same data.

Put the tablet in the world

  1. Drag the PulseTablet prefab into your scene and place it: its stand goes on a table, at about 1 m from the floor.
  2. On the root of the prefab, the FL# Script with the class Pulse.PulseTablet has a field pulse: put the Pulse object of your scene in it.

The tablet stays on its stand until someone takes it. A player takes it by one of the two grips at the bottom, or by both, with one hand each; let go, it stays where it is. Its place is the same for every player.

What the screen does:

On the screenWhat it changes
Bands: the four colored columnsDrag the white knob at the top of a column: the threshold of the band. A band above its threshold lights its knob.
Bands: the rail under the columnsDrag a knob: where one band ends and the next begins on the spectrum.
Signal: GainHow loud the music is read. The small mark in the middle is the normal level; the right end doubles it.
Signal: FadeHow long a band takes to fall back after a hit.
Signal: CurveThe shape of that fall.
Signal: Auto gainFollows the loudness of the music by itself.
The round button at the top leftTurns the analysis on or off: off, your shaders see the music stopped.
The round button at the bottom leftPuts the settings and colors back as they were when the world loaded (the on/off button stays as it is).
Colors: the four round swatchesPick one, then set it with the hue, saturation and brightness sliders under them. Picking or setting a swatch turns Auto off.
Colors: AutoThe four colors follow the notes of the music instead of the swatches.

The lower card shows the last 1.4 seconds of each band (four rows of dots, now on the right) and, under them, a wave that grows with the music.

The texts of the screen follow the language of each player's game (English or French).

Who changes what

The settings are shared: when a player moves a slider, everyone in the instance sees and hears the same. The field controllerSyncMode of Pulse.PulseTablet chooses what is shared:

controllerSyncModeShared with the instance
0 (default)everything
1everything but the on/off button
2everything but the on/off button and Gain
3nothing: each player tunes their own

Read the music in a shader

Include Pulse.cginc from the package, then read the values. A complete shader, to save as a .shader file in your project: the first color of the music glows with the bass.

hlsl
Shader "MyWorld/Pulse Glow"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Glow ("Glow", Range(0, 4)) = 1
    }
    SubShader
    {
        Tags { "RenderType" = "Opaque" }
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "Packages/com.nodeworld.pulse/Runtime/Shaders/Pulse.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _Glow;

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            v2f vert (appdata v)
            {
                v2f o;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_INITIALIZE_OUTPUT(v2f, o);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                float4 col = tex2D(_MainTex, i.uv);
                if (PulseIsOn())
                    col.rgb += PulseThemeColor(0) * PulseSmooth(0, 8) * _Glow;   // the first color of the music, with the smoothed bass
                return col;
            }
            ENDCG
        }
    }
}
FunctionReturns
PulseIsAvailable()true once the engine runs in the world. In the Unity Editor outside Play mode it is false, and every function returns 0.
PulseIsOn()true when the engine runs and the analysis is on (tablet's on/off button).
PulseBand(band, delay)The level of a band (0 bass, 1 low mids, 2 high mids, 3 treble), from 0 to 1, delay steps of 1/90 s ago (0 now, up to 127: 1.41 s).
PulseSmooth(band, smoothing)The level of a band, smoothed: 0 very smooth (0.55 of a change is left after one second) to 15 lively.
PulseChrono(mode, band, wrap)A musical time (mode 0 to 7) that grows faster when the band is loud. wrap is 14400000 or 628319: the value comes back to 0 there.
PulseThemeColor(i)Color i (0 to 3): the notes that dominate the music, or the swatches of the tablet.
PulsePhase()Where the 90 Hz clock of the history is between two steps (0 to 1): reads the history at the same place from one frame to the next.
PulseData(xy)The raw texel (x, y) of the data texture (table below).
PulseLerp(xy)The same, between two texels of a row (x with a fraction).

The data texture

The engine computes the global texture _PulseTex (128 × 32 floats) on the graphics card every frame, as AudioLink does: its script only sends the music's samples and the settings. The bands, the theme colors and the autocorrelation follow the notes one frame later. Its places, as #define in Pulse.cginc:

PlaceContents
PULSE_POS_BANDS (rows 0 to 3)Band b on row b; x = delay in steps of 1/90 s; r = level.
PULSE_POS_SPECTRUM (rows 4 and 5)Note n (0 to 239, 24 per octave from 13.75 Hz) at (n % 128, 4 + n / 128); r raw, g filtered and equalized, b filtered.
PULSE_POS_THEME (row 23, x 0 to 3)The four theme colors (rgb).
PULSE_POS_PHASE (23, x 4)r = phase of the history clock.
PULSE_POS_STATE (23, x 5)r = 1 analysis on, 0 off.
PULSE_POS_AUTOCORR (row 27)Autocorrelation of the spectrum (r).
PULSE_POS_SMOOTH (rows 28 to 31, x 0 to 15)Band b on row 28 + b; x = smoothing; r = level.
PULSE_POS_CHRONO (rows 28 to 31, x 16 to 23)Band b on row 28 + b; x = 16 + mode; r = time modulo 14 400 000, g = modulo 628 319.
PULSE_POS_VU (28, x 24 to 27)RMS level, four speeds (r).

The engine's fields

The FL# Script of the Pulse prefab (Pulse.PulseAudio) starts with these values; the tablet changes them during the game.

FieldWhat it does
sourceThe Audio Source of the music.
engineThe PulseEngine custom render texture of the package (Runtime/Textures), already set in the prefab: the graphics card computes _PulseTex in it.
gainInput level (1 by default).
bass, trebleWeight of the low and high notes of the spectrum (1 by default).
x0 to x3Where each band begins on the spectrum.
threshold0 to threshold3Threshold of each band.
fadeLength, fadeExpFalloffHow long and with which curve a band falls back.
autogain, autogainDerateAutomatic gain, and how much it holds back.
actifAnalysis on (true) or off.
themeColorMode, customThemeColor0 to customThemeColor30: colors from the notes; 1: the four chosen colors.

Licence

The package is under the MIT licence. Its LICENSE.md also holds the notices of the code it is derived from (MIT) and of its font, Geist (SIL Open Font License).