Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
# Changelog

## 0.61.0-test1 — Passages: one idea at a time

- The show plays in passages of 16 or 32 bars. Each passage has one spatial idea: one group leads it, others
support it, the rest hold a quiet figure. A passage begins, develops and ends; it does not restart every few bars.
- Scenes, room compositions, single-colour blocks and bar figures change with the passage instead of on their own
clocks. Raw / Deep / Hypnotic no longer changes look, movement, gate, colour spread and beam every eight bars.
- Five stage figures that read with the hall beams and the stage row off: crossing planes and a held asymmetric
rake from the back towers, columns that slowly open and lean into the hall, and a diagonal cross and a widening
fan from the roof spots. With the hall fader down, the stage carries every passage.
- Fewer whole-rig brightness steps with the strobes off: only the leading group pulses with the kick, holding
groups keep their light, the stage no longer switches halves every two beats, and the foreground accent moves
every four bars instead of every two beats. The screen dims while the stage leads.
- Episodes (lightning, depth chase, spatial sequence) only enter at the start of a passage. A depth chase starts as
an aligned wave, develops two variants and returns to the wave.
- Repetition memory looks at figure, lean and roles; a colour change is not a new passage.
- Listening hears six parts of the track from the spectrum (kick, bass, body, lead, clap, hats): how loud each
is, when it hits and when it comes in as a new layer. A new lead or body opens the figure, the bass widens it.
- Groove: the bars tick with the hats, and without part data the beat grid plays hats on the off-beat.
- Techno and Hard: the heads stand still. Each passage holds a position for a quarter of its length and moves only
in the dark. The rhythm is in the light: the leading group plays a beat-locked chase (across, from the centre
out, bottom to top, odd/even, ping-pong), quarters, and eighths late in a peak. The passage opens and closes
with everything on.
- The roof spots weave instead of crossing on one line, and the support figures keep each beam in its own lane,
so beams no longer pile up into one knot from the DJ's view.
- Depth chases and lightning come by more often in Raw / Deep / Hypnotic.
- `tools/measure_show.gd` measures a sixteen-minute generated set: changes per minute, changes at once, light
jumps with the strobes off, stage-head level and quick returns.

## 0.60.0-test3 — Roof bars and upper back-tower spots

- Adds eight individually addressable LED bars along the stage roof, evenly placed between the existing washes.
Expand Down
2 changes: 1 addition & 1 deletion project.godot
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ config_version=5
[application]

config/name="Stage Visualizer"
config/version="0.60.0"
config/version="0.61.0-test1"
run/main_scene="res://main.tscn"
config/features=PackedStringArray("4.7", "Forward Plus")

Expand Down
5 changes: 5 additions & 0 deletions scripts/listen.gd
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ var bass := 0.0 # 0..1, the three bands
var mid := 0.0
var treble := 0.0
var accent := 0.0 # 1 on a high hit, falling away
var parts := ListenParts.new() # kick, bass, body, lead, clap, hats: level, hits and layers coming in
var hype := 0.0 # stays up for a while after a drop
var ev_drop := false # this frame: the kick came back after a break (the drop)
var ev_broke := false # this frame: a break began
Expand Down Expand Up @@ -184,6 +185,7 @@ func stop() -> void:
mid = 0.0
treble = 0.0
accent = 0.0
parts.reset()
hype = 0.0
kick = false
begin_frame()
Expand Down Expand Up @@ -225,6 +227,7 @@ func update(dt: float) -> void:

func begin_frame() -> void:
kick = false
parts.clear_events()
ev_drop = false
ev_broke = false
ev_back = false
Expand Down Expand Up @@ -265,6 +268,7 @@ func _hear(bands: Array, rms: float, t: float) -> void:
ev_lift = ev_lift or ev.lift
ev_silent = ev_silent or (ev.silent and not was_silent)
accent = maxf(accent, a.accent)
if bands.size() >= 9: parts.update(bands.slice(3), t, STEP, clampf((rms - 0.0025) / 0.0075, 0.0, 1.0))
bass = a.bass
mid = minf(1.0, a.mid)
treble = a.high
Expand Down Expand Up @@ -391,6 +395,7 @@ func _reset_detection() -> void:
_tempo_hits = 0

func _new_track() -> void:
parts.new_track()
_reset_detection()
_listener.new_track()
_stable_bpm = 0.0
Expand Down
5 changes: 4 additions & 1 deletion scripts/listen_ear.gd
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,10 @@ func push(samples: PackedFloat32Array, step: Callable) -> void:
if count >= next:
next += every
var rms: float = spectrum()
step.call([band(35, 170), band(250, 3000), band(4000, 14000)], rms, count / rate)
# the three bands of SV's listening, then the six parts (listen_parts.gd)
var bands := [band(35, 170), band(250, 3000), band(4000, 14000)]
for b in ListenParts.BANDS: bands.append(band(b[0], b[1]))
step.call(bands, rms, count / rate)

func skip(frames: int) -> void:
count += frames
Expand Down
74 changes: 74 additions & 0 deletions scripts/listen_parts.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
class_name ListenParts
extends RefCounted
# The parts of a track, from six bands of the spectrum: how loud each is against the fullest it has been in this
# track, a hit when it strikes (a kick, a clap, a hat), and when a part comes in as a new layer.
# Bands, not separated instruments: a hat and a shaker share the top band, a lead and a vocal the middle one.

const NAMES := ["kick", "bass", "body", "lead", "clap", "hats"]
const BANDS := [[40.0, 90.0], [90.0, 250.0], [250.0, 600.0], [600.0, 2500.0], [2500.0, 5500.0], [8000.0, 15000.0]]
const THRESHOLD := [0.25, 0.22, 0.18, 0.22, 0.3, 0.12] # how hard a rise must be to count as a hit
const GAP := [0.12, 0.1, 0.07, 0.09, 0.2, 0.05] # seconds before the same part can hit again

var level := PackedFloat32Array([0, 0, 0, 0, 0, 0]) # 0..1 against this track's fullest
var hit := PackedFloat32Array([0, 0, 0, 0, 0, 0]) # 1 on a hit, falling away quickly
var present := PackedFloat32Array([0, 0, 0, 0, 0, 0]) # 0..1, how much the part plays over the last seconds
var came_in := [false, false, false, false, false, false] # this frame: the part came in as a layer
var hits := PackedInt32Array([0, 0, 0, 0, 0, 0]) # hits counted, so a show can walk on with each

var _fast := PackedFloat32Array([0, 0, 0, 0, 0, 0])
var _slow := PackedFloat32Array([0, 0, 0, 0, 0, 0])
var _bed := PackedFloat32Array([0, 0, 0, 0, 0, 0])
var _ref := PackedFloat32Array([1e-4, 1e-4, 1e-4, 1e-4, 1e-4, 1e-4])
var _yard := PackedFloat32Array([1e-4, 1e-4, 1e-4, 1e-4, 1e-4, 1e-4])
var _armed := [true, true, true, true, true, true]
var _last := PackedFloat32Array([-9, -9, -9, -9, -9, -9])
var _out_for := PackedFloat32Array([0, 0, 0, 0, 0, 0])

static func _follow(value: float, target: float, dt: float, seconds: float) -> float:
return value + (target - value) * (1.0 - exp(-dt / maxf(seconds, 0.0001)))

func reset() -> void:
for i in 6:
level[i] = 0.0
hit[i] = 0.0
present[i] = 0.0
came_in[i] = false
_fast[i] = 0.0
_slow[i] = 0.0
_bed[i] = 0.0
_ref[i] = 1e-4
_yard[i] = 1e-4
_out_for[i] = 0.0

# Forget the yardstick of the last track (a new track can be quieter or fuller).
func new_track() -> void:
for i in 6: _yard[i] = maxf(1e-4, _bed[i])

# One step of sound: raw band magnitudes, the time on the sound clock, the step length.
func update(raw: Array, t: float, dt: float, gate: float) -> void:
for i in 6:
var x: float = raw[i] if i < raw.size() else 0.0
_ref[i] = maxf(1e-4, _follow(_ref[i], x, dt, 0.025 if x > _ref[i] else 8.0))
_fast[i] = _follow(_fast[i], x, dt, 0.012)
_slow[i] = _follow(_slow[i], x, dt, 0.2)
_bed[i] = _follow(_bed[i], x, dt, 1.5)
_yard[i] = maxf(_bed[i], _yard[i] * exp(-dt / 90.0))
level[i] = _follow(level[i], clampf(pow(x / _ref[i], 0.75), 0.0, 1.0) * gate, dt, 0.02 if x > _slow[i] else 0.12)
var rise := maxf(0.0, (_fast[i] - _slow[i]) / _ref[i]) * gate
if rise < THRESHOLD[i] * 0.4: _armed[i] = true
if _armed[i] and rise > THRESHOLD[i] and t - _last[i] > GAP[i]:
_armed[i] = false
_last[i] = t
hit[i] = maxf(hit[i], clampf(rise * 1.6, 0.4, 1.0))
hits[i] += 1
hit[i] *= exp(-dt * 14.0)
var p := clampf(_bed[i] / _yard[i], 0.0, 1.0) * gate
# a layer comes in when the part rises well above where it was for a while
if p < 0.3: _out_for[i] += dt
elif p > 0.6:
if _out_for[i] > 4.0: came_in[i] = true
_out_for[i] = 0.0
present[i] = p

func clear_events() -> void:
for i in 6: came_in[i] = false
1 change: 1 addition & 0 deletions scripts/listen_parts.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://b42xtdfy24pmt
Loading
Loading