Commit f5d34615 by PLN (Algolia)

feat(tools+tracks): the last loose sources come aboard

Four kinds of orphan, all real source, none of it generated:

- tools/sweep_state.py — 'the surface needs a sweep', as a fact the rig can
  act on. Companion to the LCXL3 driver thread.
- armada/tide-table/kick_bass.py — the deterministic kick-vs-bass tiebreaker,
  the one split CLAP cannot do reliably because both live low. Exactly the
  'validate by analysis, never infer role from the name' rule in code.
- armada/tide-table/calibration.stem.json — joins calibration.json and
  calibration.whole-mix.json, which were already tracked. A calibration set
  missing one of its three members is a trap for whoever reads the other two.
- install_superdirt.sclang — pins SuperDirt v1.7.2 and recompiles. The boot
  path is exactly where 'simple, non-brittle, one entrypoint' matters.
- live/collab/benoit/tous_ensemble.tidal and live/midi/nova/nujazz/
  nouvelle_couleur.tidal — two scores. nouvelle_couleur carries a '-- url:'
  header naming its backdrop source, per the scene standard.
- visuals/{maze,nightly_repairs,there,there0}.js — four hydra sketches.

Python entrypoints staged 100755 via --chmod, not left to the working tree.

    the drawer is empty now.
    everything in it was something,
    which is why it took three weeks to look.
parent a14aee74
...@@ -73,3 +73,7 @@ pulsar-tidalcycles ...@@ -73,3 +73,7 @@ pulsar-tidalcycles
# 69 MB of rendered slop clips in 22 mp4s — same rule as visuals/scenes. # 69 MB of rendered slop clips in 22 mp4s — same rule as visuals/scenes.
visuals/slop/out/ visuals/slop/out/
# Bakeoff viewer for the clips in visuals/slop/out/ — an index for media git
# does not hold, so it would point at nothing on a fresh clone.
visuals/slop/bakeoff/
{
"source": "stem",
"n": 24,
"valence": {
"a": 1.9843079852190033,
"b": -0.46265592724146076
},
"arousal": {
"a": 2.530493967929303,
"b": -0.34947941219527806
},
"raw": {
"va_err": 0.639460094317664,
"quad": 0.375,
"emo_hit": 0.2916666666666667
},
"loo": {
"va_err": 0.6182347805216091,
"quad": 0.4583333333333333,
"emo_hit": 0.2916666666666667
}
}
\ No newline at end of file
#!/usr/bin/env python3
"""Deterministic kick↔bass tiebreaker — the one confusion CLAP can't reliably split.
Kick and (sub)bass both live low in the spectrum, so the zero-shot classifier
(sample_classify) routinely flips them. The MDA (feature_eda) named the family
discriminators that DO separate them — temporal_centroid + decay_slope (PC4,
envelope) and spectral_centroid (register, PC1) — so we break the tie by physics,
not by prompt.
The nuance the rule must survive (the whole reason a single feature won't do): a
bass *stab* (jvbass) is transient like a kick — short, fast decay, energy front-
loaded — so envelope alone votes "kick" for it. What still separates them is
**tonalness**: a kick is a broadband percussive thump (atonal, no stable pitch); a
bass — even a stab — carries a pitch in the bass register. So the decision is
register/percussiveness + tonalness, with the envelope as a secondary vote.
Thresholds are CALIBRATED from name-confident ground truth, not guessed:
python3 kick_bass.py calibrate # distributions + separation on GT folders
python3 kick_bass.py one <file|folder> # show features + verdict
Imported by sample_classify.classify_files: it only engages when the top-2 CLAP
families are {kick, bass} and close, so the librosa cost is paid on ambiguous
files only. See [[feedback_mastering_eda]] (validate by analysis, never by name)
and [[feedback_build_katana_first]] (calibrate before trusting)."""
import sys
from pathlib import Path
import numpy as np
import sample_features as SF
# Discriminators, in the order the MDA ranked them (RF #1/#2 + register + tonalness).
DISC_KEYS = ["temporal_centroid", "decay_slope_db_s", "sustain_ratio",
"spectral_centroid", "pct_percussive", "spectral_flatness", "f0_median"]
# ── thresholds (calibrated 2026-06-22 from name-confident kick vs bass folders;
# see `calibrate`). Each is a soft vote, not a hard gate — the stab case needs the
# weighted sum, because no single feature cleanly separates a bass stab from a kick.
T = {
"perc_kick": 0.62, # pct_percussive ≥ → leans kick (broadband transient)
"perc_bass": 0.45, # pct_percussive ≤ → leans bass (more harmonic)
"sustain_bass": 0.33, # sustain_ratio ≥ → held → bass
"sustain_kick": 0.22, # sustain_ratio ≤ → pure transient → kick
"tcen_kick": 0.085, # temporal_centroid (s) ≤ → energy front-loaded → kick
"decay_kick": -55.0, # decay_slope dB/s ≤ (steeper) → kick
"cen_kick": 1500.0, # spectral_centroid Hz ≥ → too bright for a sub → kick-ish
"flat_kick": 0.05, # spectral_flatness ≥ → noisier/broadband → kick
"f0_bass_lo": 30.0, # a stable f0 in [lo,hi] (bass register) is strong → bass
"f0_bass_hi": 300.0,
}
def discriminators(path):
"""Slim feature read for the tiebreak — reuse sample_features' DSP, skip the
expensive MFCC/chroma block. Returns the DISC_KEYS dict (missing keys omitted)."""
import librosa
y = SF._load(path)
if y is None:
return None
f = dict(SF._env_features(y, SF.SR)) # temporal_centroid, decay, sustain…
S = np.abs(librosa.stft(y, n_fft=2048, hop_length=512)) ** 2
freqs = librosa.fft_frequencies(sr=SF.SR, n_fft=2048)
Sm = S.mean(axis=1)
f["spectral_centroid"] = float((freqs * Sm).sum() / (Sm.sum() + 1e-12))
f["spectral_flatness"] = float(librosa.feature.spectral_flatness(y=y).mean())
try:
yh, yp = librosa.effects.hpss(y)
eh, ep = float(np.sum(yh ** 2)), float(np.sum(yp ** 2))
f["pct_percussive"] = float(ep / (eh + ep + 1e-9))
except Exception:
pass
try:
f0 = librosa.yin(y, fmin=30, fmax=2000, sr=SF.SR)
f0 = f0[np.isfinite(f0)]
if f0.size:
f["f0_median"] = float(np.median(f0))
except Exception:
pass
return {k: f[k] for k in DISC_KEYS if k in f}
def tiebreak(feats):
"""{discriminators} → ("kick"|"bass", score, reasons). score>0 ⇒ kick, <0 ⇒ bass.
A weighted vote so the bass-stab (transient-but-tonal) case still lands on bass."""
s, why = 0.0, []
def vote(w, cond, tag):
nonlocal s
if cond:
s += w
why.append(tag)
perc = feats.get("pct_percussive")
sus = feats.get("sustain_ratio")
tcen = feats.get("temporal_centroid")
decay = feats.get("decay_slope_db_s")
cen = feats.get("spectral_centroid")
flat = feats.get("spectral_flatness")
f0 = feats.get("f0_median")
# Tonalness is the strongest signal (it's what saves the bass stab from envelope).
if perc is not None:
vote(+1.5, perc >= T["perc_kick"], "percussive→kick")
vote(-1.5, perc <= T["perc_bass"], "harmonic→bass")
if f0 is not None:
vote(-1.5, T["f0_bass_lo"] <= f0 <= T["f0_bass_hi"], "bass-f0→bass")
# Envelope: kick is a front-loaded transient with a steep decay.
if sus is not None:
vote(+1.0, sus <= T["sustain_kick"], "transient→kick")
vote(-1.0, sus >= T["sustain_bass"], "held→bass")
if tcen is not None:
vote(+0.75, tcen <= T["tcen_kick"], "front-loaded→kick")
if decay is not None:
vote(+0.75, decay <= T["decay_kick"], "steep-decay→kick")
# Register: a sub-bass sits very low; brightness or broadband noise leans kick.
if cen is not None:
vote(+0.5, cen >= T["cen_kick"], "bright→kick")
if flat is not None:
vote(+0.5, flat >= T["flat_kick"], "noisy→kick")
return ("kick" if s >= 0 else "bass"), round(s, 2), why
# ── calibration: derive/validate thresholds from name-confident ground truth ────
def _gt_files(target_family, all_folders, cap_per_folder=8, max_files=400):
"""Name-confident files whose FILENAME parses to `target_family` (kick|bass)."""
import sample_meta as META
names = (sorted(d.name for d in META.DIRT.iterdir() if d.is_dir())
if all_folders else None)
if names is None:
import json
cv = json.load(open(Path(__file__).resolve().parent / "catalog_view.json"))
names = sorted({s for t in cv["tracks"] for s in t.get("score_sounds", [])})
out = []
for name in names:
files = META.folder_files(name)
if not files:
continue
hits = [f for f in files if META.parse_name(f.stem)["family"] == target_family]
out += hits[:cap_per_folder]
if len(out) >= max_files:
break
return out[:max_files]
def _summ(vals):
a = np.array([v for v in vals if v is not None and np.isfinite(v)])
if not a.size:
return " (none)"
p = np.percentile(a, [10, 50, 90])
return f"n={a.size:<4} mean={a.mean():>9.3f} sd={a.std():>8.3f} p10/50/90={p[0]:.2f}/{p[1]:.2f}/{p[2]:.2f}"
def cmd_calibrate(all_folders=False):
print("⛵ kick↔bass calibration — name-confident ground truth\n")
gt = {fam: _gt_files(fam, all_folders) for fam in ("kick", "bass")}
feats = {}
for fam, files in gt.items():
rows = []
for f in files:
d = discriminators(f)
if d:
rows.append(d)
feats[fam] = rows
print(f" {fam}: {len(rows)} files featurized")
print()
for k in DISC_KEYS:
print(f"· {k}")
for fam in ("kick", "bass"):
print(f" {fam:<5} {_summ([r.get(k) for r in feats[fam]])}")
# confusion of the rule itself on GT
print("\n rule accuracy on GT (should recover the obvious ones):")
for fam in ("kick", "bass"):
n = len(feats[fam])
ok = sum(1 for r in feats[fam] if tiebreak(r)[0] == fam)
print(f" {fam:<5} {ok}/{n} = {ok/n*100:.0f}%" if n else f" {fam}: no data")
def cmd_one(target):
p = Path(target)
files = [p] if p.is_file() else (SF.folder_files(target) or [])[:SF.MAX_FILES]
if not files:
print(f"no file/folder for {target!r}")
return
for f in files:
d = discriminators(f)
if not d:
print(f" {f.name}: (decode fail)")
continue
fam, score, why = tiebreak(d)
feats = " ".join(f"{k.split('_')[0]}={d[k]:.2f}" for k in DISC_KEYS if k in d)
print(f" → {fam.upper():<4} (s={score:+.2f}) {f.name}\n {feats}\n {', '.join(why)}")
def main():
args = sys.argv[1:]
if args and args[0] == "calibrate":
cmd_calibrate(all_folders="--all" in args)
elif args and args[0] == "one" and len(args) > 1:
cmd_one(args[1])
else:
sys.exit("usage: kick_bass.py [calibrate [--all] | one <file|folder>]")
if __name__ == "__main__":
main()
Quarks.checkForUpdates({Quarks.install("SuperDirt", "v1.7.2"); thisProcess.recompile()})
do
setcps (90/60/4)
d1
$ midiOn "^41" (<| "k*4")
$ midiOff "^41" (<| "k . k(3,8)")
$ "[jazz,kick:4]"
d2 $ gF1 $ gM1
$ midiOn "^41" (<| "~ c ~ c")
$ midiOff "^42" (<| "k . k(3,8)")
$ "cp"
# lpf 400
d3 $ gF1 $ gM1
$ "hh(8,16,1)"
# "hh"
# gain 1.4 # cut 3
# pan "<0.2!4 0.8!4>"
d7 $ gF3 $ gM3
$ off "0.25" ( -- CHIPMUNK GLUCKSMAN
(# cut 71)
. (fix (# gain 0) (n "<0>"))
. (ply 2)
. (|+ note 12)
. ( # pan 0.8)
. (|* gain (slow 16 $ range 0.5 0.9 sine))
)
$ midiOn "^91" (ply "4 <4!3 8>")
-- $ mask "<f f f t>"
$ fix (
(# end 0.5)
. (stutWith "4" "e" (
(# begin 0.3)
))
) (n 1)
$ n "<0 ~ 0 1 2 3 ~ ~>"
# "gluck"
# cut 7
# gain 1.7
# room 0.8 # sz 0.8 # dry 0.8
d4 $ gF2 $ gM3
$ slow 2
$ slice 4 "<0 1 2 3>"
$ "gfunk_bass:3"
# cut 4
# gain 1.4
# crushbus 41 (range 16 3.5 "^52")
# pan 0.3 # room 0.3 # dry 0.7
d5 $ gF3 $ gM3
-- $ slow 2
$ note
("<gs4 cs5 b4 fs4>"
+ (
arp "pinkyup"
-- "[c4,c3'maj'4]"
"[c3'min'4]"
)
-- - 12
)
# "FMRhodes1"
# gain 1.7
# modIndex (range 0 8 "^53")
# room 0.8 # dry 0.9 # sz 0.5
# pan 0.6
d8 $ gF1 $ gM1
-- $ midiOff "^60" (mask "t(4,8,1)") -- Techno drum mask
$ midiOn "^36" (loopAt 2 . (# "jungle_breaks:22")) -- Raise COMEON!
$ midiOn "^56" (loopAt 2 . (# "jungle_breaks:23")) -- Raise COMEON!
$ midiOn "^92" ( -- Bouton Nassim <3
slice 16 "[0 .. 7] . <[0 .. 7]!3 [0 1 . [2 3]]>"
. loopAt 1 . (# "breaks165")
-- . (# octer 0.4) . ( octersubsub 4)
. (# lpf 2500)
. (# room 0)
)
$ chop 16
$ "jungle_breaks:74"
# cut 8
# octersub 0.5
# gain 1.2
once $ "ho:3*32" # gain 1.6
# cut 4
once $ "gfunk_bass:0"
# gain 1.4
# room 0.4
# cut 4
once $ n 1 # "gluck"
# gain 1.3
# cut 1
# end 0.48
-- url: "/home/pln/Downloads/PerfectSunriseLoop.gif"
-- À Mon Amour
-- <3
do
resetCycles
setcps (120/60/4)
let modIndex = pF "modIndex"
d1 $ gF1 $ gM1
-- $ mask "t t t f"
$ midiOn "^41" (struct "t t t*<1!6 2 1> t*<1!7 2>")
$ midiOff "^41" (struct "t . ~ t ~ <~!3 t*<2 [2 1] [4 2] [2 4]>>")
$ stack [
"kick" # lpf 10000 # cut 1,
"rumble" |* gain 1.3
# lpf 200
# cut 11,
""
]
# "[techno:1,jazz]"
# gain 1.4
-- # cps ((quantise 1 (range 60 180 "^29"))/60/4)
d2 $ gF1 $ gM2
$ midiOn "^42" (<|"~ s ~ s*<1 2>")
$ midiOff "^42" (<| "~ s")
$ "[snare:24]"
# gain 1.4
d3 $ gF1 $ gM2 -- Cymbales sol
$ midiOn "^43" (<| "~ cy ~ cy ~ cy ~ <cy [~ cy]>")
$ midiOff "^43" (<| "~!7 <~ <cy ~> ~ cy>")
$ "cy" # "<cy:1!6 cy:2!2>" # "h2ogmcy"
# "h2ogmhh"
-- |* gain (range 1.1 0.8 perlin)
-- |* legato (range 0.5 2 (fast 2 perlin))
d4 $ gF2 $ gM3 -- Bassline
$ midiOn "^44" (struct "t(<3 5>,8)")
$ midiOn "^76" (ply 4)
$ slice 4 "<[0 1] [2 3]>"
$ "nujazz_bassp120" # n "<17 17 17 18>"
# cut 4
# gain 1.4
# room 0.8 # sz 0.5 # dry 0.9
# octersub 0.6
d8 $ gF1 $ gM1
$ loopAt 2
$ chop 8
$ midiOn "^60" (mask "t(4,8,<1!3 0>)" . chop 16)
$ midiOn "^92" (mask "t(16,32)" . ply "2 2 <4 8 16 16> 2")
$ midiOn "^36" (loopAt 2 . (# n 119))
$ midiOn "^56" (# n 121)
$ "jungle_breaks:120"
# cut 8
d5 $ gF3 $ gM3
$ midiOn "^57" (iter "4 1")
$ midiOn "^89" (ply "<2!3 4>")
$ loopAt 2 -- fixme why so slow? :)
$ n "<1!3 7>"
# "nujazz_guitar120"
# cut 5
# pan 0.3
# room 0.4 # sz 0.3 # dry 0.4
d6 $ gF3 $ gM3
$ slow 2
$ slice 2 "<0 1>"
$ loopAt 2
$ n "<10!8 9!8>"
# "nujazz_keys120"
# cut 5
# pan 0.8 # sz 0.7 # room 0.6 # dry 1.1
#!/usr/bin/env python3
"""sweep_state — "the surface needs a sweep", as a fact the rig can act on.
WHY
---
PLN, 2026-07-29, after the remap made four orbits misbehave in a row:
*"indeed i do this sweep usually before a gig. can the parvagues indicator slowly
flash purple when this needs be done, either at gig init, or after your changes?
an indicator helps :)"*
The sweep exists because **a remap moves the CC, but it does not move the KNOB.**
There are no motors on an LCXL, so every physical position survives a surface
change and silently acquires a new meaning. That is the single root cause behind
everything he heard that session: d9's layer gone on `wap` (A5 left at 0, now a
multiplicative gain — see pvlint PV009), d5 dead (its buttons moved 58->57 and
90->89, so a latched BT6 went dead), d8 chopped (a latched ^60 now masking). Same
failure as Ardour's fader raised by mouse while the physical fader sat at 0.
The sweep — move every mapped control through its range and back — reconciles the
two. It was a habit living only in PLN's head. This makes it a state.
THE MODEL
---------
One flag with a REASON, and it clears itself CONTROL BY CONTROL as he sweeps:
needed_since when the sweep became necessary
reason "boot", or "remap: 96 substitutions", ... shown to the human
swept_at set when every mapped control has been moved since needed_since
Deliberately NOT stored: which controls have been touched. That is live state and
belongs to whatever is watching MIDI right now (lcxl-leds already tracks it from
its read-only aseqdump). Persisting it would go stale the instant the daemon
restarts, and a stale "already swept" is the dangerous direction — it converts an
unknown into false reassurance, the same rule as gig-log preflight's exit 2.
Set it from:
tools/migrate-columns.py --apply (a remap invalidates every position)
gig-up.sh (fresh boot: seeds are neutral, knobs are not)
Read it from:
tools/lcxl-leds.py (slow purple pulse on unswept controls)
tools/gig-log.py preflight (reports it in the pre-gig gate)
USAGE
python3 tools/sweep_state.py --status
python3 tools/sweep_state.py --need "reason"
python3 tools/sweep_state.py --done
"""
from __future__ import annotations
import argparse
import json
import os
import pathlib
import sys
import time
STATE_DIR = pathlib.Path(
os.environ.get("XDG_STATE_HOME", pathlib.Path.home() / ".local" / "state")
) / "parvagues"
STATE_FILE = STATE_DIR / "sweep.json"
def _read() -> dict:
try:
return json.loads(STATE_FILE.read_text())
except Exception:
# No file, unreadable, or corrupt -> "we do not know". Default to NEEDED:
# an unknown sweep state must never present as swept. Cheap to clear (one
# sweep), expensive to get wrong (a silent orbit mid-set).
return {}
def status() -> dict:
"""-> {'pending': bool, 'reason': str, 'needed_since': float|None, 'age': float}"""
d = _read()
since = d.get("needed_since")
swept = d.get("swept_at")
pending = since is not None and (swept is None or swept < since)
if since is None and swept is None:
# Never recorded either way. Treat as pending, with an honest reason.
return {"pending": True, "reason": "no sweep ever recorded",
"needed_since": None, "age": 0.0}
return {
"pending": pending,
"reason": d.get("reason", "unknown"),
"needed_since": since,
"age": (time.time() - since) if since else 0.0,
}
def mark_needed(reason: str) -> None:
"""Record that the surface no longer matches the map."""
d = _read()
d["needed_since"] = time.time()
d["reason"] = reason
STATE_DIR.mkdir(parents=True, exist_ok=True)
STATE_FILE.write_text(json.dumps(d, indent=2) + "\n")
def mark_done() -> None:
"""Record that every mapped control has been moved since the flag was set."""
d = _read()
d["swept_at"] = time.time()
STATE_DIR.mkdir(parents=True, exist_ok=True)
STATE_FILE.write_text(json.dumps(d, indent=2) + "\n")
def describe() -> str:
s = status()
if not s["pending"]:
return "surface sweep: done"
age = s["age"]
when = (f"{age/3600:.1f}h ago" if age >= 3600
else f"{age/60:.0f}m ago" if age >= 60
else "just now") if s["needed_since"] else "unknown"
return f"surface sweep NEEDED ({s['reason']}, since {when})"
def main() -> int:
ap = argparse.ArgumentParser()
g = ap.add_mutually_exclusive_group(required=True)
g.add_argument("--status", action="store_true")
g.add_argument("--need", metavar="REASON")
g.add_argument("--done", action="store_true")
a = ap.parse_args()
if a.need:
mark_needed(a.need)
print(f" {describe()}")
elif a.done:
mark_done()
print(f" {describe()}")
else:
print(f" {describe()}")
return 1 if status()["pending"] else 0
return 0
if __name__ == "__main__":
sys.exit(main())
s0.initImage("/home/pln/Work/Hydra/zephyr/maze.png")
s1.initImage("/home/pln/Work/Hydra/StarryNights/img/september-13-2019-arches-cluster.jpg")
// s1.initCam(1)
src(s0)
// .modulate(noise())
.scale(0.7)
// .repeatX(2)
.scroll(0,0.13)
.out(o0)
src(o0)
.add(src(s1)
.scale(() => 0.6 + 0.2 * Math.sin(time/10))
.luma(() => 0.9 + 0.5 * Math.sin(time / 13))
.brightness(() => -0.2 + 0.2 * Math.sin(time / 20))
)
.mask(src(s0).luma(0.5).invert())
.out(o1)
src(o1)
.scale(0.9)
.out(o2)
src(o2)
.modulate(src(s0).scrollY(0.2).scale(() => 0.2 + 0.1 * Math.cos(time / 100))) // FOR 408.jpg
.modulate(src(s0).sub(src(o2).scroll(0.6)).colorama(2), 0.8)
.scale(() => 2.5 + 0.5 * Math.sin(time/12))
.add(noise(100,0.2).luma(0.9).scale(0.5))
.out(o3)
render(o3)
a.show()
a.setCutoff(3.5)
a.setScale(5.8)
a.setBins(4)
a.setSmooth(0.65)
voronoi(10,() => 0.001 * (10 + a.fft[0]+a.fft[1]+a.fft[2]+a.fft[3]))
.modulate(osc(1 + 100 * a.fft[2],0.02))
.scale(() => 0.9 + 0.25 * a.fft[0])
.mult(gradient(0.1)
.hue(() => 0.3 + 0.3 * a.fft[3])
.modulateRotate(noise())
)
.out()
src(o0)
// .brightness(-0.3)
.rotate(() => (time / 4 % 360))
.modulate(src(o1).scale(0.99), () => 0.1 + 0.089 * (a.fft[2] + 0.5 * a.fft[3]))
.out(o1)
src(o1)
.blend(src(o1).scroll(0.1,0))
.blend(src(o1).scroll(0.21,0),() => 0.4 * a.fft[3])
.blend(src(o1).scroll(0.1,-0.2),() => 0.2 * a.fft[2])
.out(o2)
src(o2)
.brightness(() => -0.4 + 0.3 * a.fft[0])
.out(o3)
render(o3)
// MOUNTAIN
s0.initImage("https://images.unsplash.com/photo-1554629947-334ff61d85dc?q=80&w=3769&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D")
// COFFEE
// s0.initImage("https://images.unsplash.com/photo-1554629947-334ff61d85dc?q=80&w=3769&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D")
// SUNSET
// s0.initImage("https://images.unsplash.com/photo-1495567720989-cebdbdd97913?q=80&w=3870&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D")
src(s0)
// .modulate(noise())
.scale(1.7)
// .repeatX(2)
.scroll(0,0.13)
.brightness(() => - 0.8)
.out(o0)
src(o0)
.add(src(s0)
.scale(() => 0.6 + 0.2 * Math.sin(time/10))
.luma(() => 0.99 - 0.2 * a.fft[3] - 0.05 * a.fft[0])
// .brightness(() => -0.2 + 0.01 * a.fft[3] + 0.12 * a.fft[2] + 0.05 * a.fft[0])
)
.mask(src(s0).luma(0.5).invert())
.out(o1)
src(o1)
.scale(2.9)
.scroll(1,-0.05)
.out(o2)
src(o2)
// .modulate(src(s0).scrollY(0.2).rotate(() => Math.cos(time / 100)), 0.4 * (a.fft[0] + a.fft[1] + a.fft[2] + a.fft[3])) // FOR 408.jpg
// .modulate(src(s0).sub(src(o2).scroll(0.6)).colorama(2), () => 0.001 + 0.1 * a.fft[2])
.scale(() => 1.2 + 0.5 * Math.sin(time/12) + 0.1 * a.fft[0])
.add(noise(100,0.2).luma(0.9).scale(0.5),
() => 0.4 + 2 * a.fft[0] + a.fft[3]
)
.blend(src(o1).colorama(0.0001)
.rotate(() => ([0,0,0,time].fast(1/16) % 360)) // TODO Rotate
,() => 0.8 * a.fft[2] + 0.2 * a.fft[3])
.out(o3)
render(o3)
a.setBins(4)
a.show()
a.setScale(4)
a.setCutoff(1)
a.setSmooth(0.5)
// MOUNTAIN
s0.initImage("https://images.unsplash.com/photo-1554629947-334ff61d85dc?q=80&w=3769&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D")
// COFFEE
// s0.initImage("https://images.unsplash.com/photo-1554629947-334ff61d85dc?q=80&w=3769&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D")
// SUNSET
// s0.initImage("https://images.unsplash.com/photo-1495567720989-cebdbdd97913?q=80&w=3870&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D")
src(s0)
// .modulate(noise())
.scale(1.7)
// .repeatX(2)
.scroll(0,0.13)
.brightness(() => - 0.8)
.out(o0)
src(o0)
.add(src(s0)
.scale(() => 0.6 + 0.2 * Math.sin(time/10))
.luma(() => 0.99 - 0.2 * a.fft[3] - 0.05 * a.fft[0])
// .brightness(() => -0.2 + 0.01 * a.fft[3] + 0.12 * a.fft[2] + 0.05 * a.fft[0])
)
.mask(src(s0).luma(0.5).invert())
.out(o1)
src(o1)
.scale(2.9)
.scroll(1,-0.05)
.out(o2)
src(o2)
// .modulate(src(s0).scrollY(0.2).rotate(() => Math.cos(time / 100)), 0.4 * (a.fft[0] + a.fft[1] + a.fft[2] + a.fft[3])) // FOR 408.jpg
// .modulate(src(s0).sub(src(o2).scroll(0.6)).colorama(2), () => 0.001 + 0.1 * a.fft[2])
.scale(() => 1.2 + 0.5 * Math.sin(time/12) + 0.1 * a.fft[0])
.add(noise(100,0.2).luma(0.9).scale(0.5),
() => 0.4 + 2 * a.fft[0] + a.fft[3]
)
.blend(src(o1).colorama(0.0001)
.rotate(() => ([0,0,0,time].fast(1/16) % 360)) // TODO Rotate
,() => 0.8 * a.fft[2] + 0.2 * a.fft[3])
.out(o3)
render(o3)
a.setBins(4)
a.show()
a.setScale(4)
a.setCutoff(1)
a.setSmooth(0.5)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment