Commit cb010b96 by PLN (Algolia)

feat(armada): tidal→tracks tooling — locate-matrix, sample TF-IDF, pydantic models, master EDL

First versioned import of L'Armada (media/marketing toolbox) plus this session's
tidalcycles→tracks tooling work. Audio kept out of git (armada/.gitignore).

Tooling:
- tools/sample_tfidf.py — sample TF-IDF over the local .tidal corpus (772 docs).
  Vocabulary = local Dirt-Samples folder names (custom packs symlinked in);
  sound-context extraction (s/sound/#) so mininotation booleans don't pollute.
  Derives discriminative samples: PunkAChien = vec1_acid(df18)+cpluck(df23);
  jungle_breaks(df78) is common filler. → sample_tfidf.json (validated).
- armada/tide-table/models.py — shared pydantic v2 models: Provenance, LocateCell,
  MasterEDL/MasterEdit, TfidfReport. Typed, JSON in/out, provenance-carrying.
- armada/tide-table/locate-matrix.md + build_track_recording_map.py — L0 metadata
  map (site tracklists × take_gig_map), canonical track id = .tidal path.
- armada/tide-table/seed_edl_take89.py + master_edl_take89.json — PLN's phone+
  WH-1000XM5 resplit review as 9 typed edits (3 bad_cut = detector ground-truth).
- armada/serve.py — Range-capable LAN static server for phone audition.
- armada/ui/ — Vite+React+TS+Tailwind Judge pilot (Ship's Bridge design system);
  PRODUCT.md + DESIGN.md design language.

Correction captured: the "Hamburg PunkAChien" was a misID (Insouciance→Liquid
Finale @ 39C3); real 38C3 PunkAChien = Take35 (pending ear-verify). meth_bass/d6
silence is a controller-ergonomics artifact, not a track tell — dropped as a
fingerprint feature. See performance_notes.md, tasks/010 + 011.
parent 9e23f95b
# L'Armada — keep audio + build artifacts out of git (text/code/data only).
# Audio (stems, masters, fragments, proxies, mixes) — large, regenerable from source.
*.flac
*.wav
*.mp3
*.aif
*.aiff
*.m4a
*.ogg
*.opus
# Audio working dirs (belt-and-suspenders)
tide-table/punkachien/stems/
tide-table/punkachien/hamburg_stems/
tide-table/punkachien/masters/
tide-table/punkachien/frags/
# Python
__pycache__/
*.pyc
# Node / Vite (also covered by ui/.gitignore)
ui/node_modules/
ui/dist/
*.log
# Playwright screenshots / scratch
ui/screenshots/
*.tmp
---
name: L'Armada
description: Instrument-grade dark UI for the ParVagues catalog & mastering toolbox — the ship's bridge.
colors:
# Brand (from the ParVagues site: styles/globals.css) — reserved, not data
brand-magenta: "#d900ff"
brand-magenta-low: "#a700d1"
brand-magenta-deep: "#8900b3"
# Surfaces (tonal layering, dark)
surface: "#0a0a0a"
surface-raised: "#111111"
surface-overlay: "#171717"
hairline: "#ffffff1f"
# Ink
ink: "#e8e8ea"
ink-muted: "#9a9aa0"
ink-faint: "#6a6a70"
# Role families (orbit register groups) — always paired with label + glyph + lane
role-percs: "#ff8c00"
role-bass: "#7c5cff"
role-melodic: "#36c5f0"
role-tops: "#2dd4bf"
role-atmos: "#8a93a6"
role-vox: "#ff3d7b"
# Lifecycle status — always paired with a glyph
status-idea: "#737373"
status-wip: "#e0a82e"
status-ready: "#5bc091"
status-released: "#d900ff"
status-blocked: "#ff5252"
status-archived: "#4a4a4a"
typography:
display:
fontFamily: "Geist, Inter, system-ui, sans-serif"
fontSize: "clamp(1.5rem, 3vw, 2.25rem)"
fontWeight: 600
lineHeight: 1.1
letterSpacing: "-0.02em"
title:
fontFamily: "Geist, Inter, system-ui, sans-serif"
fontSize: "1.125rem"
fontWeight: 600
lineHeight: 1.25
letterSpacing: "-0.01em"
body:
fontFamily: "Geist, Inter, system-ui, sans-serif"
fontSize: "0.9375rem"
fontWeight: 400
lineHeight: 1.5
letterSpacing: "normal"
label:
fontFamily: "Geist, Inter, system-ui, sans-serif"
fontSize: "0.75rem"
fontWeight: 500
lineHeight: 1.2
letterSpacing: "0.01em"
data:
fontFamily: "Geist Mono, ui-monospace, SFMono-Regular, monospace"
fontSize: "0.8125rem"
fontWeight: 500
lineHeight: 1.2
letterSpacing: "normal"
rounded:
sm: "4px"
md: "6px"
lg: "10px"
full: "999px"
spacing:
xs: "4px"
sm: "8px"
md: "12px"
lg: "16px"
xl: "24px"
xxl: "32px"
components:
button-primary:
backgroundColor: "{colors.ink}"
textColor: "{colors.surface}"
rounded: "{rounded.md}"
padding: "8px 16px"
typography: "{typography.label}"
button-ghost:
backgroundColor: "{colors.surface-raised}"
textColor: "{colors.ink}"
rounded: "{rounded.md}"
padding: "8px 16px"
typography: "{typography.label}"
button-accent:
backgroundColor: "{colors.brand-magenta}"
textColor: "{colors.surface}"
rounded: "{rounded.md}"
padding: "8px 16px"
typography: "{typography.label}"
role-pill:
backgroundColor: "{colors.surface-raised}"
textColor: "{colors.ink}"
rounded: "{rounded.full}"
padding: "2px 8px"
typography: "{typography.label}"
status-chip:
backgroundColor: "{colors.surface-raised}"
textColor: "{colors.ink-muted}"
rounded: "{rounded.sm}"
padding: "2px 6px"
typography: "{typography.label}"
meter-readout:
backgroundColor: "{colors.surface}"
textColor: "{colors.ink}"
rounded: "{rounded.sm}"
padding: "2px 6px"
typography: "{typography.data}"
---
# Design System: L'Armada
## 1. Overview
**Creative North Star: "The Ship's Bridge"**
L'Armada's tools are the bridge of a ship: a calm command station where the operator
reads the sea-state at a glance and acts with confidence. Instruments are dark, exact,
and always-on — charts, meters, signal flags. Nothing shouts; the important thing is
that every reading is *honest*, because decisions (ship this take, cut here, this master
wins) ride on it. The ParVagues glitch DNA is the occasional signal flare, not the
wallpaper: a flash of brand magenta on the *now*, an earned bit of motion. It earns its
place or it's cut.
The system is built for **one expert user, listening while reading**, often for hours on
a studio monitor and sometimes glancing at a phone over LAN. So the eye is the *second*
channel to the ear: the screen confirms and directs, it never competes. Density is
welcome — this is product UI, not a landing page — but density without noise. Two layout
laws follow from this: a **persistent instrument rail** (transport, the orbit-group rail,
identity + calibration, loudness) that never moves, and a **mode-aware workspace** whose
emphasis shifts through the mastering arc — *Audition → Judge → Cut → Decide* — surfacing
spectral detail, A/B diff, boundary tools, or ship controls only when that task is live.
Calm at rest; detail on demand.
This system explicitly rejects: SaaS-cream landing slop (gradient text, glassmorphism,
hero-metric templates, per-section uppercase eyebrows), skeuomorphic plugin chrome
(fake-metal knobs, brushed-aluminium bezels, faux-3D), and the generic Bootstrap
dashboard (blue primary buttons, endless identical card grids, no point of view). The
reference feel is **Linear / Vercel** restraint, **Grafana** observatory data-viz for the
timelines and heatmaps, and **Ableton** dark studio precision.
**Key Characteristics:**
- Dark tonal surfaces, hairline borders, flat — no decorative shadows.
- One grotesk (Geist) for everything human; one mono (Geist Mono) for *data only*
meters, timecodes, orbit IDs, LUFS. Mono is the instrument readout, never the body.
- A single semantic vocabulary — role families, lifecycle status, takes, gigs — reused in
every tool, so learning one instrument teaches them all.
- Color always reinforces a label/glyph/position; never the only signal.
- Brand magenta is rare by law (the One Voice Rule) — it marks the *now* and the brand.
## 2. Colors
A near-black bridge lit by instrument colors: a deep dark base, a disciplined set of
**role-family hues** for orbit/register encoding, a muted **lifecycle** scale, and a
single reserved **brand magenta**.
### Primary
- **Brand Magenta** (#d900ff, ramp #a700d1 → #8900b3): The ParVagues signal. Reserved
for the *now* (playhead / active marker), the single most important action on a screen,
and "released" lifecycle. Its rarity is the meaning — see the One Voice Rule.
### Secondary — Role Families (the orbit-register vocabulary)
Each orbit is placed in a **measured** register family (by spectral centroid /
fundamental, never by sample name). Same hue across every tool. Always shown *with* the
family label + a glyph + its lane position, so hue is reinforcement, not the only signal.
- **Percs** (#ff8c00, ▰): kicks, snares, hats, percussion — transient, warm.
- **Bass** (#7c5cff, ▂): sub, acid, reese/wobble, bass-spine plucks — low, deep.
- **Melodic** (#36c5f0, ♪): leads, keys, riffs, plucks in the high register — bright.
- **Tops** (#2dd4bf, ≈): breaks, chops, texture loops — busy, mid-high.
- **Atmos** (#8a93a6, ◌): pads, drones, ambient, fx — diffuse, background.
- **Vox** (#ff3d7b, ◍): vocal samples and chops — human, forward.
### Tertiary — Lifecycle Status
For catalog / triage / release tools. Muted by default; always carries a glyph.
- **Idea / raw** (#737373, ·) · **WIP** (#e0a82e, ◐) · **Ready / mastered** (#5bc091, ●)
· **Released** (#d900ff, ★) · **Blocked** (#ff5252, ✕) · **Archived** (#4a4a4a, ▢).
### Neutral
- **Surface** (#0a0a0a): the bridge floor — app background.
- **Surface Raised** (#111111): panels, the instrument rail, cards.
- **Surface Overlay** (#171717): dialogs, popovers, menus.
- **Hairline** (#ffffff1f): borders and dividers — structure by line, not by shadow.
- **Ink** (#e8e8ea): body and primary text. **Ink Muted** (#9a9aa0): secondary/labels.
**Ink Faint** (#6a6a70): disabled, watermarks, axis ticks.
### Named Rules
- **The One Voice Rule.** Brand magenta covers ≤10% of any screen. It is the *now* and the
brand and the single primary action — nothing else. If two things are magenta, one is wrong.
- **The Never-Hue-Alone Rule.** Every role / status / identity is carried by label + glyph
+ position, with color as reinforcement. The UI must stay fully legible in greyscale.
- **The Measured-Register Rule.** An orbit's family comes from analysis (centroid /
fundamental / band energy), never from the sample's name. The map is per-track.
- **The Calm Default Rule.** A view at rest shows only the persistent instruments. Spectral
detail, boundary tools, provenance, and A/B diff surface on task, hover, or mode — then recede.
## 3. Typography
**Display / Body Font:** Geist (with Inter, system-ui fallback) — a neutral modern
grotesk, the Linear/Vercel register. One family, hierarchy by scale + weight.
**Data / Mono Font:** Geist Mono (with ui-monospace fallback) — *only* for instrument
readouts: timecodes, LUFS, dBFS, Hz, orbit IDs, take IDs. Tabular, so digits don't jitter
as they update. Using mono for data (not for prose) gives the instrument-grade read
without tipping into terminal-cosplay.
- Scale ratio ≥1.25 between steps; hierarchy through weight contrast, not many sizes.
- Body line length capped 65–75ch in any prose (notes, descriptions).
- `text-wrap: balance` on titles. No all-caps body; uppercase only on short ≤4-word labels.
- Numeric readouts use `font-variant-numeric: tabular-nums`.
## 4. Elevation
**Flat, with tonal layering.** Depth is communicated by surface tone (#0a0a0a → #111111 →
#171717) and hairline borders, not by ambient shadows. This matches the Ableton/Linear
posture and keeps the bridge calm.
- The persistent instrument rail sits on `surface-raised` with a single hairline edge.
- **Shadows are structural only**, never decorative: a real overlay that floats above the
app (dialog, popover, the section-edit menu) gets one soft shadow to signal "this is
detached." Inline cards and panels get a hairline, no shadow.
- Semantic z-index scale (never arbitrary 9999): `base → rail → sticky-ruler →
popover → modal-backdrop → modal → toast → tooltip`.
## 5. Components
Variants are sibling keys (`button-primary`, `button-primary-hover`). Shadows, focus
rings, and motion live in prose / the CSS layer, not the 8-prop token schema.
- **Buttons.** `button-primary` = bright ink fill on dark (the Linear "white button"),
for the main affordance per view. `button-ghost` = raised surface + hairline, for
secondary actions. `button-accent` = brand magenta, used at most once per view for the
single most consequential action (ship, commit cut). Focus: 2px magenta ring at low
alpha, offset 2px. Hover: one tonal step lighter. All have a `prefers-reduced-motion`
instant state.
- **Role Pill.** `[glyph] LABEL ·NN` — family glyph, family label, orbit number in mono.
Background = surface-raised; the family hue is a 2px left-inset tick + the glyph color
(never a full side-stripe border — see Don'ts). Used in the orbit-group rail and anywhere
an orbit is named.
- **Status Chip.** `[glyph] label` in lifecycle color over surface-raised. Catalog/triage.
- **Meter.** Horizontal or vertical bar + a mono readout (`-9.0 LUFS`). The bar fills in
ink; target/ceiling marks are hairlines; over-target is the one place `status-blocked`
red is allowed inline. Momentary value animates, the numeral updates ≤10/s.
- **Orbit-Group Rail.** The signature instrument. Five labeled lanes (Percs · Bass ·
Melodic · Tops · Atmos; Vox folds into Melodic's stack when present). Each lane holds the
role-pills of the orbits *active in the current track*, lit at the playhead by their RMS.
This is the per-track "what's sounding now," grouped by measured register.
- **Waveform + Regions.** Built on wavesurfer.js v7: waveform in ink-faint, played portion
in ink, playhead in brand magenta. **Regions = sections** (draggable/resizable, labelled,
loopable) and **boundary markers** (a region with zero width). Styled via `::part()`.
- **Stem-Map Cell.** Heatmap unit: role-family hue at an alpha driven by RMS; time on X,
orbit/lane on Y. Greyscale-legible via intensity; hue is the family reinforcement.
- **Transport.** Play/pause (Space), seek (click waveform), loop region, ±boundary step.
Always present, bottom of the instrument rail.
**Layout & motion (folded here per spec):** persistent left/bottom instrument rail +
mode-aware workspace. Flexbox for 1D rails, Grid for the 2D stem-map. Responsive without
breakpoints where possible (`minmax`). Motion is ease-out (quart/expo), short (≤200ms),
and reserved for state changes that need tracking (a region snapping, a value settling, a
panel surfacing). No bounce, no elastic, no entrance-animation reflex. Every motion has a
reduced-motion fallback (crossfade or instant).
## 6. Do's and Don'ts
**Do**
- Keep the four bridge instruments (transport, orbit-rail, identity+calibration, loudness)
visible at all times; let everything else surface by mode.
- Pair every color with a label/glyph/position. Test the UI in greyscale.
- Derive an orbit's register family from analysis, per track. Show the calibration/
provenance badge so the operator can trust the time-base.
- Use mono for numbers, the grotesk for words. Tabular numerals on anything that updates.
- Reserve magenta for the *now*, the brand, and one primary action.
**Don't**
-**Side-stripe borders** (`border-left` >1px as a colored accent). Use a 2px inset tick,
a background tint, or the glyph instead.
-**Gradient text**, **glassmorphism**, **hero-metric templates**, **per-section uppercase
eyebrows**, **numbered section scaffolding** (01/02/03). These are the generated-look tells.
- ❌ Skeuomorphic knobs/bezels or fake-3D. Meters are flat bars; controls are flat.
- ❌ Identical card grids as a default layout. Cards only when they're the right affordance;
never nested.
- ❌ Light-gray body text "for elegance." Body ink stays ≥4.5:1 on its surface.
- ❌ Color as the only carrier of meaning. ❌ Motion without a reduced-motion fallback.
- ❌ Em dashes in UI copy; buttons are verb + object ("Commit cut", not "OK").
# Product
## Register
product
## Users
A single user: **PLN / ParVagues** — a TidalCycles livecoder, performer, and
producer running his own salvage-and-release operation ("L'Armada"). Expert in the
domain (knows every orbit, sample, gig, and take); not here to be onboarded, here to
**get audio and catalog work done fast**. Context of use: long focused sessions at a
studio monitor in dim light, plus quick checks on a phone over LAN (auditioning a
master, validating a cut). Often listening *while* reading the screen — the eyes are
a second channel to the ears, never the main event.
## Product Purpose
A fleet of **internal instruments** for the ParVagues catalog: mastering A/B judges,
stem-map / boundary validators, triage and comparison screens, distribution and gig
ledgers. They turn analysis data (orbit activity, loudness, spectra, boundaries,
provenance) into something the ear and eye can **trust and act on**. Success = PLN
reaches a confident decision (ship this take, cut here, this master wins) faster, and
the tools never mislead him about what the data actually says. These are workbenches,
not a storefront; the public face is the separate ParVagues site.
## Brand Personality
**Calm, precise, quietly alive.** Instrument-grade trust first: legible meters, honest
markers, nothing shouting. Low cognitive load is the default state. ParVagues' glitch /
livecoding DNA shows up only where there's aesthetic room — a small, earned, playful
moment, never decoration that costs legibility. Voice: terse, specific, a maker talking
to himself. (Note: avoid leaning into literal terminal-cosplay — it reads as gimmick.)
## Anti-references
- **SaaS-cream landing slop** — hero-metric templates, gradient text, glassmorphism,
tiny uppercase eyebrows, the generated-look impeccable bans. This is product UI.
- **Skeuomorphic plugin chrome** — fake-metal knobs, brushed-aluminium bezels, faux-3D.
- **Generic Bootstrap dashboard** — default components, blue primary buttons, endless
identical card grids, no point of view.
- Consumer music-app gloss (Spotify/Apple-Music shine) is *not a goal* but not forbidden.
- Reference feel to aim at instead: **Linear / Vercel** product minimalism, **Grafana**-ish
observatory data-viz for the timeline/heatmap work, **Ableton** dark studio precision.
## Design Principles
1. **Trust the instrument.** Every meter, number, and marker is honest and traceable.
Surface calibration and provenance; never imply more certainty than the data has.
(A 75-second time-base bug once hid behind a confident-looking UI — never again.)
2. **The ear leads, the screen serves.** Quiet by default; show what's relevant at the
playhead and get out of the way. The tool must never compete with the audio for
attention.
3. **One language across the fleet.** Samples, orbits, roles, gigs, takes, and lifecycle
states share a single visual + semantic vocabulary, reused in every tool — learn one
instrument and you can read them all. Low cognitive load is a *system* property.
4. **Encode meaning redundantly.** Identity and state are carried by label + shape +
position, with color as reinforcement. Never hue alone — it keeps the dense role-coding
legible and colour-blind safe by construction.
5. **Glitch with intent.** ParVagues character appears in small, deliberate moments
(motion, texture, a flash of the brand magenta). If a flourish costs a millisecond of
reading speed, it's cut.
## Accessibility & Inclusion
Single-user tool — optimize for PLN's screens (studio monitor + phone) rather than broad
WCAG certification, but keep two disciplines non-negotiable because they also make the
tools *better*:
- **Never hue alone.** Role / orbit / state / lifecycle always pair color with a label,
glyph, or position. (Principle 4.)
- **Readable contrast.** Body text ≥4.5:1; no light-gray-for-elegance; must hold up on a
dim monitor and a phone in daylight.
- **Reduced motion honored.** Every animation has a `prefers-reduced-motion` fallback.
# ⛵ L'Armada ParVagues
Media-management & marketing toolbox for ParVagues — *map the Land, then make the most of it.*
Tooling lives here (versioned in the Tidal repo, like the visualizer & mastering
chain). The **data root** is the freebox `Prod/` archive
(`/mnt/freebox/PLN/Work/Sound/Prod`, canonical — fuller than the local mirror).
The **gig/setlist data** already lives in the ParVagues site
(`~/Work/Web/www/next/content/lives/` + `tracks.json` + `ontology/events.yaml`).
## The fleet — naming chart 🌊
| Module | Codename | Full title | Role | Phase |
|---|---|---|---|---|
| Catalog | `tide-table` | *Table des Vagues* | source of truth: every asset & its lifecycle state | **1** |
| Distribution | `manifeste` | — | cargo manifest + manifesto: what ships to which DSP (the RouteNote question) | **1** |
| Gigs / network / pricing | `escales` | — | port calls: pricing-calibration ledger + flotille graph | seed now |
| Diffusion / social | `sémaphore` | *Sémaphore des Ondes* (`ondin`) | POSSE multiplex: post once → IG/Mastodon/Bluesky/… | 2 |
| Vitrine / blog | `phare` | — | lighthouse showcase + content blog (folds into existing site) | later |
## Mental models
- **Catalog = inventory with lifecycle states.** Each track: `idea → demo →
performed → recorded → mastered → released-where`. Everything hangs off this.
- **Frontlist vs back-catalog.** New drops buy attention; back-catalog earns the
long tail. Cadence = steady frontlist + resurfaced backlist.
- **Each platform has a *job*.** SC = community/discovery · Bandcamp = superfans +
ownership · Spotify/DSPs = passive discovery + legitimacy · YouTube =
longform/SEO/visuals · IG = top-of-funnel · archive.org = preservation.
- **Owned vs rented audience.** Social is rented (algorithm-gated); site / email /
Bandcamp followers are owned. Convert rented → owned.
- **POSSE** (Publish on Own Site, Syndicate Elsewhere) — `sémaphore`'s design target.
- **Flotilla growth.** The network of budding artists is the #1 growth engine —
grow by sailing together, not by shouting louder. → `escales` flotille graph.
## North star (next ~6 months)
Reach + legitimacy + **gig opportunities** > superfans > "fair" revenue.
Revenue is *not* a target; fair-pay calibration (don't lose gigs to mis-pricing)
*is*. Audience: worldwide glitch/ambient/jungle/liquid/abstract electronic, FR **and** JP.
Content marketing the craft (sample selection, demucs sampling, Tidal sequencing,
performance) as long-form → atomized posts.
## Data sources to chart
- `Prod/` archive — ~25 project dirs, ~500 audio files, 60 Audacity projects
- Screen-rec pile — 45 MKVs (2025-01 → 2025-12)
- Site — ~35 lives (2022–2026) + setlists + events ontology
- External — SoundCloud, Bandcamp, YouTube, archive.org, Spotify/DSPs, IG reels
- Phone / audience / DJ-desk recordings
See `../CLAUDE.md` and the task board for the working backlog.
# Private pricing/quote data — never commit fees.
escales.yaml
*.private.yaml
# escales · gigs / network / pricing
*Port calls — each gig is a stopover; each owes a* droit de port *(harbor due = your fee).*
**Not a booking app — strategic memory** that compounds into two payoffs:
1. **Pricing oracle.** Log every inquiry's *droit de port*: `{org, type, region,
fee_asked, outcome}`. After ~a dozen entries it stops being a diary and starts
answering "what should I ask for a venue like this, here?" The recent
*"went with the house DJ for budget"* decline is data point #1 — calibrate, don't sting.
2. **Flotille graph.** The network of budding artists = the #1 growth engine. Track
collabs ↔ gigs ↔ who-opened-which-door in [`flotille.yaml`](flotille.yaml).
## Relationship to the site (don't duplicate)
The ParVagues site already holds the gig map:
`~/Work/Web/www/next/content/lives/{year}/{slug}.md` (+ `tracks.json` setlists +
`ontology/events.yaml`). `escales` is a **sidecar keyed by gig slug** that adds only
what the site lacks and shouldn't publish:
- **`fee_asked` / `outcome`** — PRIVATE. → `escales.yaml` is **gitignored**.
- **`real_gig`** — in-person/public/bookable vs stream/online/corporate (needs PLN's call).
- **`coplayers`** — who else was on the bill → flotille edges.
## Gig enrichment (scanner, TODO)
For each gig, fetch from its event page (`ctaURL` / poster URLs):
- **poster / flyer images** → reuse in site media & `phare`
- **co-artists / lineup** → flotille graph
- **venue, time, organizer** → confirm/enrich ontology
Store images under the site's `public/images/parvagues/lives/{year}/{slug}/`.
Respect source terms; cache locally; PLN confirms which gigs to enrich.
## Files
- `escales.example.yaml` — committed schema/template (anonymized).
- `escales.yaml`**gitignored**, real ledger incl. private fees.
- `flotille.yaml` — committed network graph (collaborators are already public).
# escales — port-call ledger (TEMPLATE, committed; real data lives in escales.yaml, gitignored)
#
# Keyed by site gig slug where one exists (content/lives/{year}/{slug}.md).
# Inquiries that never happened (declined/pending) have no site slug — give them a local id.
port_calls:
# --- a booked, played gig (mirrors a site live) ---
- slug: cosmicfest # matches content/lives/2025/cosmicfest.md
date: "2025-06-21"
event: cosmicfest # ontology key
kind: in-person # in-person | stream | corporate | unknown
real_gig: true # public/bookable? PLN confirms
region: FR
venue: "Labenne"
fee_asked: null # PRIVATE — fill in escales.yaml only
fee_currency: EUR
outcome: played # played | booked@X | declined:<reason> | pending | free
coplayers: [] # flotille refs
via: null # who connected you (flotille ref)
poster: null # fetched flyer path
notes: ""
# --- an inquiry that fell through (pricing-calibration data point) ---
- id: q-2026-XX-houseDJ
date: "2026-XX-XX"
event: null
kind: in-person
real_gig: true
region: FR
venue: "<org/venue>"
fee_asked: null # the ask that was 'too much'
fee_currency: EUR
outcome: "declined:budget-went-with-house-DJ"
coplayers: []
via: null
notes: "Calibration: asked too high for this org's budget anchor lower next time."
# flotille — collaborators network. DEPRIORITISED for now.
# PLN: none of the backlog [name] tags are real collabs worth listing yet.
# The ONLY real artist-collaboration so far is Baba (no joint release yet — coming).
# "Which artists were on the bill with me" is a SEPARATE, later study (gig co-players ≠ collabs).
# Sampled voices/characters are NOT collaborators. Beware false positives (nova = Novation controller).
collaborators:
- id: baba
name: "Baba"
relationship: collab
repo: "live/baba/"
status: "no joint release yet planned"
notes: "The one real artist collaboration so far."
# ---- below: NOT confirmed collaborators; parked for a later 'network' study ----
gig_coplayers: # artists who shared a bill — study later, not collabs
cosmicfest_2026: ["Hugo (guitar)", "Flo (downtempo)", "@marion.lhn (dnb DJ)", "Kevin & Pipou (visuals)"]
# extract more from site lives when/if we study the network
sample_sources: # sampled voices/characters — people NOT worked with
- "bogdan_grime ('I'm from Cardiff!')"
# backlog [Louis]/[Julien]/[Val]/[Nass] tags are unconfirmed — ignore for now
owned_events: # PLN-(co)organised — owned nodes, still useful
- id: cosmicfest
name: "CosmicFest"
location: "Labenne Océan, FR"
landing: "https://me.nech.pl/cosmicfest"
editions: [{v: v0, date: "2025-06-21"}, {v: v1, dates: "2026-08-21/23"}]
notes: "Friends weekend; seed for a recurring festival."
# manifeste · distribution
*The cargo manifest (what ships to which DSP) — and the manifesto.*
Full 2026 research: [`research-2026-distribution.md`](research-2026-distribution.md).
## Decision (2026-06)
RouteNote is **"milking but not moving"** — confirmed. Still distributes & pays,
but ~3–4+ week moderation, thin support, Trustpilot 3.3/5, and account-freeze
risk. It advances neither reach nor legitimacy (our top priorities).
**Subscription-free stack** (no DistroKid/TuneCore-classic tax):
| Layer | Tool | Why |
|---|---|---|
| DSP distribution | **CD Baby** | one-time/release ($9.99 single / $14.99 album), keep **91%**, stays up forever, reaches JP (AWA/LINE/KKBOX) |
| Superfans / ownership / gig funding | **Bandcamp** | 15%/10% cut, 0% on 8 Bandcamp Fridays, daily Stripe payouts, human-only AI policy (on-brand) |
| Community / discovery | **SoundCloud** | keep as scene hub + FPR engine; paid tier optional, **not** the distributor (it's a sub) |
| Japan push (optional) | **TuneCore Japan** | Believe subsidiary (French!), dominant inside JP; per-release annual, so JP-campaign-only |
**Rights to register** (money most indies leave on the table): **SACEM**
(authorship) + **ADAMI/SPEDIDAM** (performer) + **SCPP or SPPF** (producer —
you self-produce). Watch **CNM** *musiques actuelles* mobility/export aid for touring.
## Migration off RouteNote (no gap, low risk)
1. Export RouteNote catalog, UPCs/**ISRCs**, earnings; withdraw balance >$50.
2. Register rights (SACEM + neighbouring-rights) — distributor-independent.
3. Open **Bandcamp**, upload catalog immediately (additive, instant).
4. Set up **CD Baby**, re-deliver release-by-release, **reuse existing ISRCs** so
stream history/links don't fragment; newest/most-streamed first.
5. Once a release is confirmed live on CD Baby, **take it down from RouteNote**
(avoid DSP duplicate-delivery flags). Migrate one at a time.
6. SoundCloud unaffected throughout.
7. Reassess in 12 mo: ~50k monthly listeners → apply **AWAL**; JP traction →
**TuneCore Japan** + bilingual metadata.
## Next
Feeds off `tide-table` gap analysis → drives the **release manifeste** (task #8):
which catalog tracks ship where, in what order (frontlist + back-catalog interleave).
# Release Calendar — the manifeste (v2: live-staples first)
## Confirmed direction (PLN, 2026-06-05)
- **LEAD SINGLE: PunkAChien** 🔥 — long-awaited "fire night track", joins *Nuit Agitée*'s vibe.
Ship take = **Montreuil split** (`Montreuil26_master/tracks_bandcamp/09-PunkAChien.flac`,
4:48, 192 kHz → master to 44.1/−14 LUFS). Alt takes: Live@Hamburg (39C3), NouveauPunk (Opal).
- **Also ship as singles** (capture forgotten gold + expected bangers): Contre Visite,
It's About Time, Sunny Side Up. **Long run: release everything.**
- Take-sourcing notes:
- ⚠️ **Contre Visite** — no clean Prod recording; **extract from an Algolia set stem (2022–24)**.
- **Sunny Side Up** (light-sample, "single-and-pray") — pick: CosmicFest vs Montreuil "Super".
- **It's About Time**`AboutTime_Take1` vs `ItsAboutTime_take1_raw`.
- Sample-heavy (clear or accept risk): **Techno Orage**, **La Révolution Sera Samplée**.
**Priority = tracks you actually play live and haven't released** (2024–2026), ranked
by how many setlists they appear in. NOT the weak 2020 standalone uploads. Source:
aggregated site setlists (23 gigs) + Montreuil/CosmicFest splits, minus released
(Apple∪Deezer, aka-aware).
Sourcing: most staples already have a recording in a recent **set stem-export**
(Ardour Takes, e.g. CosmicFest=Take70, Montreuil=Take89) or a **Prod split**
(`Montreuil26_master/tracks_bandcamp/` already holds PunkAChien, Take5Drops, Super
Sunny Side Up, Desire, …). So: pick best source → tidal-ears master → ship.
## The 12 (proven set staples)
| # | Single | live plays | source / note |
|--:|---|--:|---|
| 1 | **Contre Visite** | 9 | most-played unreleased — flagship relaunch |
| 2 | **Sunny Side Up** | 9 | (Montreuil has "Super Sunny Side Up" variant) |
| 3 | **Salut Nu** | 8 | Audacity project `SalutNu_r1` exists |
| 4 | **Invoque l'Été** | 7 | → **August** drop, ties CosmicFest v1 ☀️ |
| 5 | **Permanence** | 7 | |
| 6 | **Something about Drums** | 5 | (Prod `SomethingAboutDnb`) |
| 7 | **Venons Ensemble** | 5 | on CF Bandcamp, not DSP single (come_* sample pack) |
| 8 | **Bain Électrique** | 4 | on CF Bandcamp, not DSP single |
| 9 | **PunkAChien** | 4 | Montreuil split FLAC ready |
| 10 | **Premier Septembre** (=Sept1) | 3 | → **September** 🍂; collab (Rhadamanthe) → split |
| 11 | **Acidulé** | 3 | |
| 12 | **Piment Brésilien** | 3 | Montreuil split ready; summery |
## Pool (next tier, played 2–3×)
Nightly Repair · It's About Time · Quand on Décolle · Lady Perplexity · Orage / Techno
Orage · Take 5 Drops · Aria Sans Serif · You My Sunshine · Desire · La Révolution Sera
Samplée · Premiere Grillade · Break Dynasty · Nass Revient de Mars · Love First · Ère de Jeu …
## Seasonal garnish (slot where it fits, don't force)
Invoque l'Été → Aug · Premier Septembre → Sep · Orage → autumn · summer bangers → Jul/Aug.
## Per-release pipeline
best recording (set stem / Prod split / Audacity) → **tidal-ears master** (−14 stream /
−9 club) → artwork → **CD Baby (DSP) + Bandcamp** (prefer a BC Friday) → keep SC →
**announce via sémaphore** + a sampling/sequencing content post.
## Flags
- **aka-dedup (already released, exclude):** Blue Gold = L'Or Bleu · JeuDrill = Jeudi Drill.
- **collabs → Soundrop split + flotille:** Premier Septembre, Faith in Bass, Liquid Finale.
- Many staples live *inside sets* → depend on set-splitting (#18) for clean masters,
unless a Prod split / standalone already exists.
- Confirm the 12 with PLN before committing — readiness (does a clean master exist?)
may reorder them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>L'Armada · Distribution Compass 2026</title>
<style>
:root{
--abyss:#06121c; --deep:#0b2233; --sea:#103a52; --foam:#7fe3d4; --foam2:#39c2b0;
--sun:#ffd27a; --coral:#ff7a6b; --ink:#dff1f5; --mute:#8fb3c0; --line:#1d4358;
--good:#46d39a; --bad:#ff6b6b; --warn:#ffc24d;
}
*{box-sizing:border-box}
body{
margin:0; font:16px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
color:var(--ink);
background:
radial-gradient(1200px 600px at 80% -10%, rgba(127,227,212,.10), transparent 60%),
radial-gradient(900px 500px at 0% 0%, rgba(255,210,122,.06), transparent 55%),
linear-gradient(180deg,var(--abyss),#04222e 60%,#02161f);
min-height:100vh;
}
.wrap{max-width:1180px;margin:0 auto;padding:32px 20px 80px}
header h1{font-size:clamp(26px,4vw,40px);margin:0 0 6px;letter-spacing:.5px}
header h1 .em{color:var(--foam)}
.sub{color:var(--mute);margin:0 0 4px}
.pill{display:inline-block;font-size:12px;color:var(--abyss);background:var(--foam);
border-radius:999px;padding:2px 10px;font-weight:700;letter-spacing:.4px}
.insight{
margin:24px 0;padding:16px 18px;border-left:3px solid var(--sun);
background:linear-gradient(90deg,rgba(255,210,122,.10),transparent);border-radius:0 10px 10px 0}
.insight b{color:var(--sun)}
h2{font-size:14px;text-transform:uppercase;letter-spacing:2px;color:var(--mute);margin:34px 0 12px}
/* priorities */
.prios{display:flex;flex-wrap:wrap;gap:10px}
.chip{cursor:pointer;user-select:none;border:1px solid var(--line);background:var(--deep);
border-radius:10px;padding:8px 12px;display:flex;align-items:center;gap:8px;transition:.15s}
.chip:hover{border-color:var(--foam2)}
.chip .w{font-size:11px;font-weight:800;letter-spacing:.5px;padding:1px 7px;border-radius:6px;background:#0a2c3c;color:var(--mute)}
.chip[data-w="1"]{border-color:var(--foam2)}
.chip[data-w="1"] .w{background:var(--foam2);color:var(--abyss)}
.chip[data-w="2"]{border-color:var(--sun);box-shadow:0 0 0 1px rgba(255,210,122,.25) inset}
.chip[data-w="2"] .w{background:var(--sun);color:var(--abyss)}
.hint{color:var(--mute);font-size:13px;margin:8px 0 0}
/* cards */
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(255px,1fr));gap:16px;margin-top:14px}
.card{background:linear-gradient(180deg,var(--deep),#08202e);border:1px solid var(--line);
border-radius:16px;padding:18px;display:flex;flex-direction:column;gap:10px;position:relative;overflow:hidden}
.card.lead{border-color:var(--foam)}
.badge{position:absolute;top:0;right:0;font-size:10px;font-weight:800;letter-spacing:.6px;
background:var(--foam);color:var(--abyss);padding:3px 10px;border-radius:0 0 0 10px}
.card h3{margin:0;font-size:20px}
.role{color:var(--foam);font-size:12.5px;font-weight:600;min-height:32px}
.meta{display:flex;gap:14px;flex-wrap:wrap;font-size:12.5px;color:var(--mute)}
.meta b{color:var(--ink);font-weight:700}
.fit{margin:2px 0}
.fit .bar{height:10px;border-radius:6px;background:#0a2c3c;overflow:hidden}
.fit .bar i{display:block;height:100%;border-radius:6px;
background:linear-gradient(90deg,var(--foam2),var(--foam));transition:width .35s}
.fit .lab{display:flex;justify-content:space-between;font-size:11px;color:var(--mute);margin-bottom:4px}
.fit .lab b{color:var(--foam)}
ul{margin:6px 0 0;padding:0;list-style:none;font-size:13px}
ul li{padding-left:18px;position:relative;margin:3px 0}
ul.pro li::before{content:"▲";color:var(--good);position:absolute;left:0;font-size:9px;top:4px}
ul.con li::before{content:"▼";color:var(--bad);position:absolute;left:0;font-size:9px;top:4px}
.verdict{margin-top:auto;font-size:12.5px;color:var(--ink);background:#0a2c3c;border-radius:8px;padding:8px 10px;border:1px solid var(--line)}
/* stack */
.stack{margin-top:18px;background:linear-gradient(135deg,rgba(127,227,212,.10),rgba(255,210,122,.06));
border:1px solid var(--foam2);border-radius:16px;padding:18px}
.stack h3{margin:0 0 10px;font-size:16px;color:var(--foam)}
.layers{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px}
.layer{background:var(--deep);border:1px solid var(--line);border-radius:12px;padding:12px}
.layer .l{font-size:11px;letter-spacing:1px;text-transform:uppercase;color:var(--mute)}
.layer .n{font-weight:800;font-size:17px;margin:2px 0}
.layer .d{font-size:12.5px;color:var(--mute)}
footer{margin-top:40px;color:var(--mute);font-size:12px;border-top:1px solid var(--line);padding-top:16px}
footer a{color:var(--foam2)}
.reset{cursor:pointer;color:var(--foam2);font-size:12px;border:1px solid var(--line);background:transparent;
border-radius:8px;padding:4px 10px;margin-left:8px}
</style>
</head>
<body>
<div class="wrap">
<header>
<span class="pill">⛵ L'ARMADA · MANIFESTE</span>
<h1>Distribution <span class="em">Compass</span> 2026</h1>
<p class="sub">Where should ParVagues' music live? Tune your priorities — the cards re-score live.</p>
</header>
<div class="insight">
<b>They're not rivals — they're layers.</b> A <b>distributor</b> (CD Baby) is the <i>pipe</i> that
puts you on the DSPs. <b>Spotify</b> is a <i>destination</i> on the other end of that pipe — pure
discovery &amp; legitimacy, reached <i>through</i> the distributor, with no direct fan relationship.
<b>Bandcamp</b> is your <i>store</i> for superfans &amp; gig-funding. <b>SoundCloud</b> is your scene's
<i>town square</i>. Most artists run several at once. This tool ranks <i>fit to your priorities</i>, not "the winner".
</div>
<h2>① What matters to you? <span style="text-transform:none;letter-spacing:0;color:var(--mute);font-weight:400">— click to cycle: ◌ ignore → ● matters → ★ critical</span>
<button class="reset" id="reset">reset to ParVagues defaults</button></h2>
<div class="prios" id="prios"></div>
<p class="hint">Defaults reflect your north star: no subscription, reach/legitimacy, gig &amp; superfan funnel, ownership — revenue de-emphasised.</p>
<h2>② The options, scored for you</h2>
<div class="grid" id="cards"></div>
<div class="stack">
<h3>⚓ Recommended stack (subscription-free)</h3>
<div class="layers">
<div class="layer"><div class="l">DSP pipe</div><div class="n">CD Baby</div><div class="d">One-time/release, keep 91%, stays up forever, reaches Spotify/Apple + JP (AWA/LINE/KKBOX).</div></div>
<div class="layer"><div class="l">Superfans · gigs</div><div class="n">Bandcamp</div><div class="d">Own the relationship + data. 0% on the 8 Bandcamp Fridays. Where bookings &amp; real money come from.</div></div>
<div class="layer"><div class="l">Town square</div><div class="n">SoundCloud</div><div class="d">Keep as the scene hub + FPR engine. Paid tier optional — <i>not</i> your distributor.</div></div>
<div class="layer"><div class="l">Destination</div><div class="n">Spotify</div><div class="d">Discovery + the "I listen to you on Spotify" legitimacy. Fed by CD Baby. Don't expect direct $.</div></div>
</div>
<p class="hint" style="margin-top:12px">Plus: register rights (SACEM + ADAMI/SPEDIDAM + SCPP/SPPF) &amp; watch CNM mobility aid. Leave RouteNote release-by-release once each is live on CD Baby.</p>
</div>
<footer>
<p><b>Sources (2026):</b>
<a href="https://www.musicbusinessworldwide.com/spotify-to-launch-music-pro-service-with-superfan-perks-like-early-access-tickets-and-ai-remix-tool-for-up-to-5-99-more-per-month-report/">MBW — Spotify Music Pro</a> ·
<a href="https://newsroom.spotify.com/2026-05-21/universal-music-group-spotify-licensing-agreements-fan-made-covers-remixes/">Spotify newsroom — fan remixes</a> ·
<a href="https://musically.com/2026/05/21/spotify-will-now-reserve-concert-tickets-for-subscribers-who-are-superfans-of-artists/">Music Ally — Reserved tickets</a> ·
<a href="https://www.musicweek.com/digital/read/soundcloud-now-enables-artists-to-keep-100-of-distribution-royalties-across-streaming-platforms/092964">Music Week — SoundCloud 100%</a> ·
<a href="https://www.chartlex.com/blog/money/how-to-sell-music-on-bandcamp-2026">Chartlex — Bandcamp 2026</a> ·
<a href="https://aristake.com/digital-distribution-comparison/">Ari's Take — distributor comparison</a> ·
<a href="https://cdbaby.com/cd-baby-cost/">CD Baby pricing</a></p>
<p>Full report: <code>armada/manifeste/research-2026-distribution.md</code>. Generated 2026-06-04.</p>
</footer>
</div>
<script>
// ---- model ----
const DIMS = [
{k:"nosub", label:"No subscription", def:2},
{k:"reach", label:"Reach & legitimacy", def:2},
{k:"superfan",label:"Superfans & gigs", def:2},
{k:"own", label:"Own the relationship",def:2},
{k:"japan", label:"Japan reach", def:1},
{k:"revenue",label:"Direct revenue", def:0},
{k:"free", label:"Zero cost", def:1},
{k:"ease", label:"Simplicity", def:1},
];
// scores 0..5 per platform per dim
const P = [
{id:"cdbaby", name:"CD Baby", lead:true, badge:"THE PIPE",
role:"DSP distributor — puts you on Spotify/Apple/+ JP",
cost:"$9.99 single / $14.99 album (one-time)", cut:"keeps 9%",
s:{nosub:5,reach:5,superfan:1,own:3,japan:5,revenue:3,free:1,ease:4},
pro:["One-time fee, music stays up forever","Reaches every DSP incl. Japan's AWA/LINE/KKBOX","No recurring 'tax' — keep 91%","Reuse ISRCs when migrating off RouteNote"],
con:["Per-release cost (not free)","Support quality has slipped","It's plumbing, not a fan channel"],
verdict:"Your DSP backbone. The clean no-sub way onto Spotify & the world."},
{id:"bandcamp", name:"Bandcamp", badge:"THE STORE",
role:"Direct-to-fan store — superfans, ownership, gig funding",
cost:"Free to start", cut:"15% digital (10% after $5k), 0% on BC Fridays",
s:{nosub:5,reach:3,superfan:5,own:5,japan:4,revenue:5,free:4,ease:4},
pro:["You own the fan relationship + data","Best direct pay (~80–85%)","Human-only AI policy (on-brand)","Strong JP import/physical culture","8 zero-fee Bandcamp Fridays in 2026"],
con:["Niche discovery vs mainstream","Songtradr leans toward sync-licensing — keep backups","Less 'legitimacy varnish' than Spotify"],
verdict:"Where bookings and real money come from. Your superfan engine."},
{id:"soundcloud", name:"SoundCloud", badge:"TOWN SQUARE",
role:"Community + discovery hub (your scene) + FPR",
cost:"Free tier; Artist Pro ~$99/yr", cut:"0% on DSP (since Nov 2025) for Pro",
s:{nosub:2,reach:5,superfan:4,own:3,japan:3,revenue:3,free:3,ease:3},
pro:["Your scene already lives here (glitch/jungle/liquid)","FPR pays 2–4× Spotify on native plays","Now keeps 100% of DSP royalties (Pro)","Direct fan support + merch added in 2026"],
con:["Paid tiers are subscriptions (your dislike)","FPR only on SoundCloud-native plays","Don't make it your DSP distributor"],
verdict:"Keep as the community hub. Pay only for the FPR/analytics, not distribution."},
{id:"spotify", name:"Spotify", badge:"DESTINATION",
role:"DSP destination — discovery & legitimacy, NOT sell-direct",
cost:"Free to be on (need a distributor)", cut:"~pennies/stream",
s:{nosub:3,reach:5,superfan:2,own:1,japan:4,revenue:1,free:5,ease:3},
pro:["Max discovery + 'I listen on Spotify' legitimacy","Spotify for Artists analytics (free)","Present in Japan"],
con:["Can't publish directly — needs a distributor (CD Baby)","Superfan/Music Pro perks are platform-controlled, US/major-first","No fan data or relationship — rented audience","Per-stream pay is negligible"],
verdict:"A destination you reach via CD Baby — for reach, not revenue."},
];
const STORE="armada_compass_weights";
let W = load();
function load(){try{const s=JSON.parse(localStorage.getItem(STORE));if(s)return s;}catch(e){} return defaults();}
function defaults(){const o={};DIMS.forEach(d=>o[d.k]=d.def);return o;}
function save(){try{localStorage.setItem(STORE,JSON.stringify(W));}catch(e){}}
function renderPrios(){
const el=document.getElementById("prios"); el.innerHTML="";
DIMS.forEach(d=>{
const w=W[d.k];
const c=document.createElement("div");
c.className="chip"; c.dataset.w=w;
c.innerHTML=`<span class="w">${w===0?"◌":w===1?"●":"★"}</span><span>${d.label}</span>`;
c.onclick=()=>{W[d.k]=(W[d.k]+1)%3; save(); render();};
el.appendChild(c);
});
}
function fit(p){
let num=0,den=0;
DIMS.forEach(d=>{const w=W[d.k]; num+=w*(p.s[d.k]||0); den+=w*5;});
return den===0?0:Math.round(100*num/den);
}
function renderCards(){
const ranked=[...P].map(p=>({p,f:fit(p)})).sort((a,b)=>b.f-a.f);
const top=ranked[0].f;
const el=document.getElementById("cards"); el.innerHTML="";
ranked.forEach(({p,f})=>{
const lead=f===top && top>0;
const card=document.createElement("div");
card.className="card"+(lead?" lead":"");
card.innerHTML=`
${p.badge?`<span class="badge">${p.badge}</span>`:""}
<h3>${p.name}</h3>
<div class="role">${p.role}</div>
<div class="meta"><span>💸 <b>${p.cost}</b></span><span>✂️ ${p.cut}</span></div>
<div class="fit">
<div class="lab"><span>fit to your priorities</span><b>${f}%</b></div>
<div class="bar"><i style="width:${f}%"></i></div>
</div>
<ul class="pro">${p.pro.map(x=>`<li>${x}</li>`).join("")}</ul>
<ul class="con">${p.con.map(x=>`<li>${x}</li>`).join("")}</ul>
<div class="verdict">${p.verdict}</div>`;
el.appendChild(card);
});
}
function render(){renderPrios();renderCards();}
document.getElementById("reset").onclick=()=>{W=defaults();save();render();};
render();
</script>
</body>
</html>
# Independent Music Distribution in 2026 — Decision Report for ParVagues
*France-based · electronic/livecoding (TidalCycles) · glitch/ambient/jungle/liquid ·
global + Japan · SoundCloud-rooted · refuses monthly subscriptions ·
reach + legitimacy + gigs > superfans > revenue*
> Research conducted 2026-06-04 by a background research agent (web sources cited).
> Summary decision lives in [`README.md`](README.md).
## Executive summary
RouteNote still distributes and pays, but in 2025–2026 it is **stagnant, slow,
support-thin**: Trustpilot ~3.3/5 (**23% one-star**), moderation **~17–22 working
days (3–4+ weeks)**, and a recurring pattern of accounts frozen over "artificial
streams" with earnings withheld and no real appeal. Not a scam, not abandoned —
but it builds neither **reach nor legitimacy**, your top priorities.
Best structure = a **two-layer stack, neither a monthly sub**:
1. **DSP distribution** via one-time-per-release distributor — **CD Baby** (keep
91%, stays up forever), **TuneCore Japan** if Japan becomes a real target.
2. **Direct-to-fan** via **Bandcamp** (superfans, ownership, gig-funding) +
**SoundCloud** (community hub; paid tier optional).
Avoid: DistroKid / TuneCore-classic / Amuse / Ditto / LANDR / Symphonic-Starter
(all recurring, or Ditto's payment red flags). AWAL/Believe selective, not yet accessible.
## 1. RouteNote reality check
Distributes & pays, **but slowly and with downside risk**.
- Trustpilot **3.3/5**, ~2,750 reviews, 59% 5★ but **23% 1★** (bimodal).
- Moderation slowed: **~17–22 working days**; user reports of 30–45 days common.
- **Anti-fraud freezes:** documented cases of releases pulled platform-wide citing
"artificial/matched streams," earnings withheld ($82–$3,000+), no appeal.
- Support thin (free tier explicitly lower priority).
- Pricing (no monthly sub): **Free** = keep 85% (15% rev-share), permanent hosting.
**Premium** ≈ $12.99/single, $59.99/album, **annual renewal** for 100%, $50 min payout.
- **Verdict:** "milking but not moving" is fair. Free 85% + permanent hosting is its
only genuinely attractive trait, matched/beaten elsewhere with less risk.
## 2. Alternatives by payment model
### A. One-time per release (best fit)
| Distributor | Upfront | Cut | Payout | Notes |
|---|---|---|---|---|
| **CD Baby** | $9.99 single / $14.99 album | **9%** (keep 91%, permanent) | $25 min | Broad DSP reach incl. Japan. **Top pick.** |
| **Soundrop** | ~$0.99/track | 15% | — | Covers/splits — useful for collab tracks needing payee splits. |
### B. Free rev-share
| Distributor | Cut | Notes |
|---|---|---|
| RouteNote (free) | 15% | §1 — stagnant/slow/freeze risk. |
| Amuse (free) | — | **Dormant** since 2024; old free accounts can't release. Treat as gone. |
| YouAux / Vibeable | 0% | 2024–25 entrants, unproven; higher legitimacy risk than CD Baby. |
### C. Recurring (rejected — for completeness)
DistroKid ($24.99/yr, music pulled if you stop) · TuneCore classic ($9.99/yr single)
· Ditto (£19–29/yr, **reliability red flags — avoid**) · LANDR ($23.99+/yr, 15% if you
cancel) · Symphonic ($19.99/yr Starter, Beatport access, selective) · Amuse paid ($23.99+/yr).
### D. Selective / invite-only
AWAL (Sony) — application-only, ~<10% acceptance, wants ~50k+ monthly listeners; future
target. Believe (Euronext Paris; owns TuneCore) — B2B, reach via TuneCore.
**Best "no sub + keep royalties":** CD Baby (91%, one-time) — cleanest match.
## 3. SoundCloud 2026
Distributes to DSPs (60+) via SoundCloud for Artists. **Dropped its 20% cut end-Nov
2025** — Next Plus/Artist Pro keep 100%. But it's a **subscription** (Artist Pro
~$99/yr). **FPR** (Fan-Powered Royalties) pays 2–4× Spotify per-stream **but only on
SoundCloud-native plays**, not distributed DSP streams. **Keep as community + FPR
hub; don't route DSP delivery through it (recurring cost).**
## 4. Bandcamp 2026
Healthier than the 2023 panic suggested. Epic→Songtradr (2023, ~50% layoffs) was the
trauma; 2026 is operational/stabilizing: Stripe migration (no payout fees, 135+
currencies, daily payouts), **"Keeping Bandcamp Human" AI policy (Jan 2026)** bans
AI-generated music (pro-artist signal), **8 Bandcamp Fridays in 2026** (Feb 6, Mar 6,
May 1, Aug 7, Sep 4, Oct 2, Nov 6, Dec 4; 0% fee those days). Economics: 15% digital
(10% after $5k/yr), 10% physical, ~80–85% net. **Risk:** Songtradr leans on catalog as
sync-licensing pool; keep masters + store-data backups. **Your superfan/gig-funding layer.**
## 5. France / EU rights & funding
- **SACEM** — authorship/composition (quarterly).
- **Neighbouring rights:** performer = **ADAMI** &/or **SPEDIDAM** (join both for
one-person electronic); producer (self-produced = you) = **SCPP** or **SPPF**.
- **CNM** (Centre national de la musique) — *musiques actuelles* **international
mobility + development aid**, export certifications. **Most relevant France lever for
gigs + reach.**
- French distributors (Believe/Paris, Wiseband) exist but recurring/B2B — not better
than CD Baby for this model.
## 6. Japan reach
JP DSPs: Spotify/Apple/Amazon/YT JP + **AWA** (largest native), **LINE Music**,
**KKBOX**. CD Baby/TuneCore/DistroKid/IDOL all deliver there; CD Baby cited for fast
broad JP coverage (so the recommended path already reaches JP). **TuneCore Japan**
(Believe subsidiary, French) = dominant *inside* Japan, deep DSP/editorial integration
— per-release annual, so JP-campaign-only. Localization is the lever: **bilingual
(Japanese) metadata**, upload 7+ days early for editorial. Your glitch/ambient/jungle/IDM
palette has a real JP audience (Warp/IDM/breakcore lineage). Bandcamp resonates (JP
import/physical/superfan culture).
## 7. Bottom line + migration
**Stack:** CD Baby (DSPs) + Bandcamp (superfans/gigs) + SoundCloud (community/FPR) +
rights registration (SACEM + ADAMI/SPEDIDAM + SCPP/SPPF) + CNM aid on radar.
Optional JP escalation: TuneCore Japan + bilingual metadata.
**Ranked alternatives:** 2nd DistroKid (only if you'd tolerate one cheap annual fee —
violates no-sub rule); 3rd stay RouteNote-free (acceptable free fallback, accept risk).
**Migration (no gap):** export RouteNote data → register rights → open Bandcamp & upload
→ set up CD Baby, re-deliver reusing ISRCs newest-first → take each down from RouteNote
once live on CD Baby → keep SoundCloud → reassess in 12 mo (AWAL / TuneCore Japan).
## Sources
RouteNote: Trustpilot, musicdistribute.com, payusnomind.info, RouteNote Support Hub,
loop.fans, PissedConsumer. Distributors: Ari's Take (Digital Distribution Comparison
2026), Ones to Watch 2026, cdbaby.com / alera.fm, orphiq (Amuse), soundrop.com,
Indie Music Academy (AWAL), alera.fm (ownership map). SoundCloud: Music Week (Nov 2025),
SoundCloud Help, Chartlex, DJ Mag. Bandcamp: Bandcamp Help, MBW, Chartlex, TechCrunch &
Rolling Stone (2023). France: muzisecur.fr, electronicmusicfactory.com, cnm.fr,
wiseband.com. Japan: japanmusicmarketing.com, identitymusic.com, LabelGrid, musicdistribute.com.
*Caveats:* RouteNote moderation window varies by source (treat as 3–4+ weeks). Amuse free
tier reported dead but their pages are ambiguous — verify. AWAL's ~50k threshold is
community-reported. Bandcamp's long-term direction under Songtradr is a genuine
uncertainty — keep masters + backups regardless.
# sémaphore · *Sémaphore des Ondes* (`ondin`) — diffusion
*A coastal signaling station: one message hoisted, broadcast to many flags.* **Phase 2.**
The **POSSE** engine (Publish on Own Site, Syndicate Elsewhere): author once at the
canonical node (the site / `phare`), syndicate to IG, Mastodon, Bluesky (+ more).
## To spec (task #10)
- Canonical source: a post = `{body, media[], link, tags, lang}` authored once.
- Syndication targets & API reality per network:
- **Mastodon** — clean API, app tokens. Easiest.
- **Bluesky** — AT Protocol, app passwords. Easy.
- **Instagram** — main channel today; Graph API needs Business/Creator + FB
plumbing; otherwise manual/assisted. The hard one.
- Cadence: **release/month + post/week**, frontlist ⇄ back-catalog interleave,
driven off `tide-table` (task #11).
- Content pillars (atomize the craft): sample selection · demucs sampling · Tidal
sequencing · performance. Long-form (blog/`phare`) → atomized SN posts.
- Reuse setlist `tracks.json` ingredients (BPM, sample packs, Tidal code) as ready
content fuel.
#!/usr/bin/env python3
"""Tiny LAN static server with HTTP Range support (so <audio> seeking works).
Python's stdlib http.server has no Range support → mobile/desktop seeking on
FLAC/MP3 fails. This adds 206 Partial Content. Binds 0.0.0.0 so you can open
the printed LAN URL on a phone on the same wifi.
python3 serve.py --dir tide-table/punkachien --port 8731
"""
import argparse, os, re, socket
from functools import partial
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
class RangeHandler(SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header("Accept-Ranges", "bytes")
self.send_header("Cache-Control", "no-cache")
super().end_headers()
def send_head(self):
rng = self.headers.get("Range")
if not rng:
return super().send_head()
path = self.translate_path(self.path)
if not os.path.isfile(path):
return super().send_head()
m = re.match(r"bytes=(\d*)-(\d*)", rng)
if not m:
return super().send_head()
size = os.path.getsize(path)
start = int(m.group(1)) if m.group(1) else 0
end = int(m.group(2)) if m.group(2) else size - 1
end = min(end, size - 1)
if start > end:
self.send_error(416, "Requested Range Not Satisfiable")
return None
length = end - start + 1
f = open(path, "rb")
f.seek(start)
self.send_response(206)
self.send_header("Content-Type", self.guess_type(path))
self.send_header("Content-Range", f"bytes {start}-{end}/{size}")
self.send_header("Content-Length", str(length))
self.end_headers()
self._range_remaining = length
return _LimitedReader(f, length)
def copyfile(self, source, outputfile):
if isinstance(source, _LimitedReader):
remaining = source.remaining
while remaining > 0:
chunk = source.read(min(64 * 1024, remaining))
if not chunk:
break
outputfile.write(chunk)
remaining -= len(chunk)
source.close()
else:
super().copyfile(source, outputfile)
class _LimitedReader:
def __init__(self, f, remaining):
self.f, self.remaining = f, remaining
def read(self, n):
return self.f.read(n)
def close(self):
self.f.close()
def lan_ip():
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect(("192.168.1.1", 1))
return s.getsockname()[0]
except Exception:
return "127.0.0.1"
finally:
s.close()
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("--dir", default=".")
ap.add_argument("--port", type=int, default=8731)
a = ap.parse_args()
os.chdir(a.dir)
ip = lan_ip()
httpd = ThreadingHTTPServer(("0.0.0.0", a.port), partial(RangeHandler))
print(f"serving {os.getcwd()}")
print(f" local : http://127.0.0.1:{a.port}/")
print(f" LAN : http://{ip}:{a.port}/ ← open on phone (same wifi)")
httpd.serve_forever()
---
log: 001
title: "Charting L'Armada naming the fleet"
date: 2026-06-04
task: "#1 Scaffold L'Armada toolbox"
tags: [kickoff, naming, strategy]
shareable: true
---
## Cap (what & why)
Stood up a media-management & marketing toolbox for ParVagues: map everything ever
recorded/performed, then run consistent releases & diffusion. North star = reach +
legitimacy + **gigs** > superfans > revenue (revenue isn't a target).
## Manœuvre (how)
Designed it as 5 modules under one fleet, with layered sound↔sea names, versioned in
the Tidal repo (data root stays on the freebox). Captured mental models up front so the
strategy is in-repo, not in our heads.
## Prise (findings / artifacts)
- `armada/{README, tide-table, manifeste, semaphore, escales}` scaffolded.
- Names: `armada` (fleet) · `tide-table`/*Table des Vagues* (catalog) · `manifeste`
(distro) · `sémaphore des ondes` (POSSE social) · `escales` (gigs/pricing) ·
`phare` (vitrine, later).
## Sel (the shareable learning)
- The artist already reinvented **POSSE** (post-once-syndicate-everywhere) and
**frontlist vs back-catalog** without the names. Good vocabulary turns vague
instincts into a repeatable system.
- Naming is not decoration: "tide-table" (a real nautical schedule) *is* the release
calendar; "manifeste" (cargo manifest + manifesto) *is* what-ships-where. The pun
encodes the spec.
## Hameçon (hook)
"I gave my music catalog a pirate fleet — and the names did half the design work."
## Sillage (what it unlocks)
Every later task has a home and a shared language.
---
log: 002
title: "RouteNote on trial is it moving or just milking?"
date: 2026-06-04
task: "#7 Settle distribution strategy"
tags: [distribution, research, soundcloud, bandcamp, cdbaby, spotify]
shareable: true
---
## Cap (what & why)
PLN suspected RouteNote takes a cut but doesn't actually move the music. Needed a 2026
verdict + a no-subscription alternative (refuses the DistroKid tax).
## Manœuvre (how)
Fanned out a research agent across distributor reviews, platform docs, and 2026 news;
cross-checked pricing, royalty cuts, and reputation.
## Prise (findings / artifacts)
- RouteNote: distributes & pays, but **3–4+ week moderation, Trustpilot 3.3/5 (23% 1★),
account-freeze-without-appeal risk.** "Milking but not moving" = fair.
- Verdict stack: **CD Baby** (one-time/release, keep 91%, reaches JP) + **Bandcamp**
(superfans/gigs) + **SoundCloud** (community/FPR) + **Spotify** (destination only).
- France levers most indies miss: **SACEM + ADAMI/SPEDIDAM + SCPP/SPPF**, **CNM**
mobility aid. Full report in `manifeste/research-2026-distribution.md`.
## Sel (the shareable learning)
- **Distributor ≠ destination.** CD Baby is the *pipe*; Spotify is the *faucet*. You
don't "choose between" them — you reach Spotify *through* a distributor.
- Indies earn **<10%** of income from streaming in 2026. The living is the **superfan
economy** (Bandcamp-style direct), not the algorithm.
- Spotify's 2026 "Music Pro" superfan tier is **platform-controlled, US/major-first**
not a sell-direct channel for an indie.
## Hameçon (hook)
"I put my music distributor on trial. The verdict: guilty of doing nothing, slowly."
## Sillage (what it unlocks)
A concrete, gap-free migration off RouteNote (release-by-release, reusing ISRCs).
---
log: 003
title: "The Decision Compass a SPA to choose where music lives"
date: 2026-06-04
task: "#14 Build distribution decision SPA"
tags: [tooling, distribution, ux]
shareable: true
---
## Cap (what & why)
Turn the research into a thing you can *play with* to decide: SoundCloud vs Bandcamp
vs CD Baby vs Spotify, weighted by what actually matters to you.
## Manœuvre (how)
Single self-contained HTML SPA (no build, no network). Each option scored 0–5 across
8 dimensions; clickable priority chips (ignore→matters→critical) re-score the cards
live; defaults preset to the ParVagues north star; choices persist in localStorage.
## Prise (findings / artifacts)
- `manifeste/compare.html` — interactive chooser + a fixed "recommended stack" panel.
## Sel (the shareable learning)
- A comparison **table** tells; a **weighted, interactive** comparison *teaches*
because it forces the user to name their priorities, which is the real decision.
- Encoding the "layers, not rivals" insight as UI (pipe → destination + store + square)
lands harder than a paragraph.
## Hameçon (hook)
"I built a tiny app to answer one question every indie musician has: where should my
music actually live?"
## Sillage (what it unlocks)
PLN decides distribution async while the salvage continues — no blocking.
---
log: 004
title: "A pricing oracle, born from a 'no'"
date: 2026-06-04
task: "#9 Seed escales ledger + flotille"
tags: [escales, pricing, network, strategy]
shareable: true
---
## Cap (what & why)
A gig fell through — "sorry, went with the house DJ for budget." That sting is
data. `escales` turns it into a pricing-calibration ledger + a network graph.
## Manœuvre (how)
Built `escales` as **strategic memory, not a booking app**: a flat YAML ledger keyed
by gig slug (extends the site's existing gig data with the two things it lacks —
**fees** and **co-players**). Fees are private → gitignored. Flotille seeded from repo
collab dirs + the site's events ontology.
## Prise (findings / artifacts)
- `escales/{README, escales.example.yaml, .gitignore, flotille.yaml}`.
- The lost gig is data point #1: anchor lower for that org type/region next time.
## Sel (the shareable learning)
- A rejection is a **calibration sample**, not a verdict. Logged consistently, a
dozen of them become a "what should I ask?" oracle.
- The network of budding artists is the #1 growth engine — so the relationship map
deserves to be **data** (a flotilla), not just folders.
- Caught a false positive: `nova` isn't a collaborator, it's the **Novation**
controller. Verify your seed data against the human.
## Hameçon (hook)
"A promoter told me no. So I built a tool that makes the next 'no' less likely."
## Sillage (what it unlocks)
Pricing confidence + a network graph that feeds gig leads and site cross-links.
---
log: 005
title: "CosmicFest the first salvage, and an island we own"
date: 2026-06-05
task: "#3 External inventory (first worked example)"
tags: [salvage, catalog, reconciliation, escales, owned-event]
shareable: true
---
## Cap (what & why)
First real charting: take one gig (CosmicFest) and reconcile it across every place it
lives, to validate the catalog schema on live data before sweeping the whole fleet.
## Manœuvre (how)
Fetched the Bandcamp album, the site live-recap page, and the site festival landing;
cross-referenced against the existing `tracks.json` setlist. SoundCloud refused
scraping (JS app) — flagged for oEmbed/API.
## Prise (findings / artifacts)
- `tide-table/catalog.yaml``cosmicfest-2025` live-set + its 14 tracks, with durations
(Bandcamp), BPM/style (site), `aka` variants, and `performed_not_released`.
- `escales/flotille.yaml` — 5 new flotille members (Hugo, Flo, @marion.lhn, Kevin, Pipou)
+ a new `owned_events` section.
- CosmicFest **v1 = 2026-06-21** (v0 was 2025); a 2025 YouTube video still to locate.
## Sel (the shareable learning)
- **Performed ≠ released.** Stage names (*Blue Gold*, *Sept1*, *JeuDrill*) become release
titles (*L'Or Bleu*, *Premier Septembre*, *Jeudi Drill*), and some performed cues never
ship. A catalog without `aka` + a performed-vs-released gap field silently lies.
- **The highest-leverage gig is one you own.** CosmicFest started as a friends weekend —
but it's an *owned node*: your audience, your lineup, your brand, your data. Those are
worth growing, not just recording.
- **Tooling learns from contact.** SoundCloud's JS wall, the landing-vs-recap page split —
you only discover the real scraping rules by charting one thing for real first.
## Hameçon (hook)
"I went looking for one old gig in my archive — and found a festival I'd accidentally
founded."
## Sillage (what it unlocks)
A validated schema + fetch playbook to sweep all ~35 gigs; a flotille that's now a real
graph; and a live thread linking the bunker after-party to the drone-footage finale.
---
log: 006
title: "The gap: 7 on Spotify, 68 on SoundCloud, and a 21-month silence"
date: 2026-06-05
task: "#3/#6 External inventory gap analysis"
tags: [salvage, gap-analysis, distribution, soundcloud, deezer, tooling]
shareable: true
---
## Cap (what & why)
Find out what's *actually* released vs only sitting on SoundCloud — i.e. the real
release backlog — and answer "is RouteNote even working?"
## Manœuvre (how)
Katana = yt-dlp (SoundCloud refuses scraping but yt-dlp enumerates it) + the **open
Deezer API** as a zero-auth proxy for "what RouteNote pushed to DSPs" (Spotify needs
OAuth; Deezer doesn't). A Python cross-reference normalises titles (accents, emojis,
feat/version noise) and diffs SC against DSP.
## Prise (findings / artifacts)
- `tide-table/gap-report.md` (committed) + `sources/` raw caches (gitignored).
- **DSP: 7 releases, last 2024-09-06.** SoundCloud: 68 (51 tracks + 17 live-sets,
2020→2026). Bandcamp: ~5.
- **46 SC tracks not on any DSP** = the backlog. Top by plays: Automne Électrique,
I come from Cyberspace, Nass Arrive.
## Sel (the shareable learning)
- RouteNote *worked* — the artist just **stopped feeding it 21 months ago** and never
noticed. "Is my distributor broken?" was really "did I stop releasing?" The catalog
makes that invisible drift visible.
- **Use the easy door.** Deezer's open API answers the Spotify question without Spotify's
auth dance. Pick the API that doesn't fight you.
- Auto-diff gets you 90%; the last 10% is human — remixes/covers (WAP, Princess
Superstar) can't go on DSPs, "toma uno" is a take. Tooling proposes, the artist triages.
## Hameçon (hook)
"I had 7 songs on Spotify and 68 on SoundCloud — and hadn't noticed I'd quietly stopped
releasing for nearly two years."
## Sillage (what it unlocks)
Triage the backlog → a release calendar (one/month, frontlist ⇄ backlog) = the `manifeste`.
The whole machine now has fuel.
## Addendum — never trust one API (corrected 2026-06-05)
PLN caught it: *Nass Arrive* is on Spotify but **not** on Deezer. Deezer was undercounting
(7) — the **Apple iTunes Search API** (open, no auth) returned **25** DSP titles incl. the
whole Opal 2024 live album. So several "backlog candidates" were false positives (already
released). Corrected via the **union Apple ∪ Deezer**: DSP = 25 titles (last 2024-10-04),
real backlog = **41** SC tracks. Spotify's anon web token is now **TOTP-gated** (broke) —
SOTA = official Web API + `spotipy` + a free dev app. **Learning: one DSP API ≠ the
catalog; triangulate and take the union.** YouTube (22) turned out to be a *content/demo*
surface, not a release catalog.
---
log: 007
title: "Raising 63 performances from a 436 GB wreck"
date: 2026-06-05
task: "#13 Ardour stem salvage"
tags: [salvage, stems, ardour, tooling, reconstruction]
shareable: true
---
## Cap (what & why)
The single 436 GB Ardour "Tidal Multi" session is the richest stem source we have.
Get per-orbit stems out for every performance — without re-rendering or risking the session.
## Manœuvre (how)
Parsed `Tidal Multi.ardour` (5.4 MB XML) — the *index*, not the audio. Sources are
named `TakeN_Tidal X-1%L/R.wav`, so **Take = performance**. Built a reusable exporter
(`ardour_stem_export.py`) that, per take, joins each orbit's L/R interchange captures
into a stereo FLAC in the Montreuil26 standard layout
(`Prod/_stems/{date}_{take}/orbit-NN.flac` + manifest). Validated on Take3 (clean 12)
and Take18 (13-ch, Mic) before bulk.
## Prise (findings / artifacts)
- **63 performances** dated **2024-08-08 → 2026-05-22** — the complete recorded history,
as a timeline (datable onto the gig calendar: 2026-05-22=Montreuil, 2025-06-21=CosmicFest v0…).
- Full export ≈ **~22 GB** FLAC from **449 GB** raw.
- Reusable: `ardour_stem_export.py --list | --take X | --all`.
## Sel (the shareable learning)
- **The project file is an index, not the audio.** Parsing 5.4 MB of XML unlocked 436 GB —
let the metadata point you at the bytes; don't move the bytes blindly.
- **449 GB → 22 GB (0.05×).** 32-bit float WAV + many silent orbits + FLAC = ~20× crush.
"It's too big to keep" is often false once you measure.
- **Size ≠ value** (the inverse, from the same day): a 25 GB abandoned Audacity master was
garbage, while 84 KB silent-orbit stems are correct and worth keeping.
- **Name by what survives**`Take=performance`, dated by file mtime — so opaque take
numbers become a navigable, gig-mappable timeline.
## Hameçon (hook)
"My live archive was one 436-gigabyte file I was scared to touch. A 5-megabyte index
inside it let me lift out 63 whole performances as clean stems — total 22 GB."
## Sillage (what it unlocks)
Cross-reference take dates → gigs (site lives) to name the folders; stems then power
remasters, the Montreuil-standard `events.json` audio-reactive viz, and the drone videos (#15).
---
log: 008
title: "The catalog becomes a calendar"
date: 2026-06-05
task: "#8 Draft release manifeste"
tags: [release, manifeste, cadence, triage]
shareable: true
---
## Cap (what & why)
DSP releases froze in 2024-10. Turn the triaged backlog into a concrete year of
monthly singles to relaunch presence and feed the "when new music?" crowd.
## Manœuvre (how)
Ingested the `triage.html` export (48 confirmed calls) → 36 DSP-able originals/light.
Noticed the track NAMES are seasonal, so scheduled each single in its namesake
season — the discography sorts itself into a calendar.
## Prise (findings / artifacts)
- `tide-table/release_candidates.json` (36) + `manifeste/calendar.md` (12-month plan,
2026-07→2027-06, + 24-track year-2 pool).
- Month 1 = **I come from Cyberspace** (relaunch flagship); Aug ties to CosmicFest v1;
Dec = Deck the (Dance) Hall on a Bandcamp Friday.
## Sel (the shareable learning)
- **The artist already organised the calendar — in the song titles.** *Premier
Septembre, Octobre Jaune, Vie Hivernale, Battements Printaniers*… release each in its
season and the schedule writes itself, on-brand.
- **"Frozen distribution" was really "stopped releasing."** 36 finished-enough tracks
sat un-shipped. The catalog turned an invisible 21-month gap into a visible 3-year plan.
- Triage tooling proposes; the artist's ear confirms (48 calls) — then code turns calls
into a calendar.
## Hameçon (hook)
"I hadn't released on Spotify in nearly two years — turns out I had three years of
finished tracks waiting, and they'd already named their own release dates."
## Sillage (what it unlocks)
A monthly cadence the `sémaphore` (POSSE) + content engine can run on. Each release:
master (tidal-ears) → CD Baby + Bandcamp → announce + a sampling-story post.
---
log: 009
title: "Mastering from the wreck PunkAChien, by analysis not by name"
date: 2026-06-05
task: "#19 Ship PunkAChien · #13 stem export · #20 best-take"
tags: [mastering, stems, eda, tooling, punkachien, release]
shareable: true
---
## Cap (what & why)
Master the lead relaunch single, PunkAChien, from real stems — and decide between
the Montreuil and Hamburg takes — for a fresh-ears A/B the next morning.
## Manœuvre (how)
1. **Exported the stems first** (non-negotiable): the Montreuil take is `Take89` in
the Ardour wreck; only 9/65 takes had been salvaged, so ran the exporter to land
12 phase-coherent orbit-stems, then sliced the PunkAChien window
`[2387.0 → 2674.74]s` (master-time + HEAD_TRIM 75.0; the cut lands exactly on a
detected silence — independent confirmation).
2. **Read the score** (`live/collab/raph/punkachien.tidal`) for the orbit→element map,
then **threw the names away** and built the real map by **EDA on the audio** — a new
reusable `tidal-ears master eda` subcommand (spectral centroid, pyin pitch, band
energy, RMS/crest, + an activity-timeline sparkline).
3. **Mastered from stems** with a config-driven Rhadamanthe chain (`build_master.py`):
per-element HPF, kick-sidechain carve on the bass bus, mono-bass ≤120, drum widen,
glue comp, two-pass loudnorm. 4 masters + 6 loudness-matched A/B fragments + a
self-contained `judge.html`.
## Prise (findings / artifacts)
- New tool: `tidal-ears master eda` (+ `--timeline`). Working tools in
`armada/tide-table/punkachien/`: `build_master.py`, `judge.html`, `stems/`,
`masters/` (×4), `frags/` (×6), `eda_core.json`.
- Arrangement, measured: cpluck (A♯2) is the riff/bass spine; acid (D2) front-half;
**moog sub is silent until the ~208s DROP**; jungle break owns the outro.
- **`meth_bass` never plays in this take** — the code/name said "wobble bass", the
audio said silence. Caught only because EDA ran.
- Montreuil LRA 7.4 vs Hamburg 3.7 — Montreuil is the dynamic take (breakdown→drop),
Hamburg the steady 2:22 burner.
## Sel (the shareable learning)
- **Master by analysis, not by name.** The single most useful move was distrusting the
livecode: `meth_bass` was named like a monster and was pure silence; `cpluck` ("a
pluck") was the loudest thing in the track and carrying the bassline. EDA on stems
isn't optional — it's the first fader you touch.
- **Stems are the leverage.** A 2-track can only be glued; from 12 orbits you can
sidechain the bass to the kick, mono the sub, and widen only the drums. The wreck of
an 80-minute Ardour session is, per track, a fully recallable mix.
- **Tooling compounds.** The `eda` subcommand and `build_master.py` cost one track to
write and now master the next 30 for free.
## Hameçon (hook)
"The bassline in my own track wasn't the sample called 'bass'. The thing labelled
meth_bass played for exactly zero seconds. I'd never have known if I mastered by eye
instead of by ear-plus-FFT."
## Sillage (what it unlocks)
A/B verdicts (`judge.html``punkachien_verdicts.json`) feed the v2 master, then
PunkAChien ships (artwork → CD Baby + Bandcamp → announce). The `eda`/`build_master`
pair becomes `tidal-ears master track` for the whole release calendar.
---
log: 010
title: "The ear caught what the math missed"
date: 2026-06-05
task: "#19 PunkAChien · #26 resplit · #27 Hamburg stems · metadata provenance"
tags: [mastering, boundaries, calibration, provenance, tooling, stem-map]
shareable: true
---
## Cap (what & why)
Master PunkAChien properly (stem-vs-stem A/B), apply validated boundaries to re-split
the Montreuil set, and keep mastering honest — separate from metadata.
## Manœuvre (how)
Built a stem-map validation instrument (orbit-presence over time + audio player + ±5s
boundary-stepping). PLN validated cuts by ear; his timestamps disagreed with my computed
ones — which exposed a **time-base bug**: I'd assumed stem = master + 75 (a HEAD_TRIM).
The new `tidal-ears master locate` tool settled it by cross-correlation (known split sits
at proxy 2309.1s, z=29): stem ≈ master − 3. Re-windowed PunkAChien, recovered Hamburg
stems from Take87 via the same `locate`, re-split all 15 Montreuil tracks.
## Prise (findings / artifacts)
- New reusable tools: `tidal-ears master {eda, stemmap, locate}`; `build_master.py`,
`resplit_montreuil.py`; `stemmap.html`, `judge.html`. Ledger: `boundaries_take89_
validated.json`; ear-archive: `performance_notes.md`. 15 tracks → `Prod/Montreuil26_resplit/`.
- Both PunkAChien takes stem-mastered. `meth_bass` plays zero seconds; cpluck is the riff.
## Sel (the shareable learning)
- **The performer's ear is a debugger.** Two casual "I hear Piment at 6:00" comments
surfaced a 75-second systematic offset that loudness math had hidden. Build the tool that
lets the artist validate by ear, and it audits your pipeline for free.
- **Distrust derived time-bases; verify by content.** A cross-correlation `locate` against a
known-good clip beats any HEAD_TRIM arithmetic.
- **Mastering ≠ metadata.** A hallucinated venue ("La Cour des Lézards" vs real **Les
Nouveaux Sauvages**) taught: store facts with provenance (`type:locator` + `as_of`), pull
gig metadata from the canonical site source, never invent — never store self-notes in data.
## Hameçon (hook)
"My own ear caught a 75-second bug in my mastering pipeline before any meter did — by
noticing a sample came in 'a little early'."
## Sillage (what it unlocks)
A trustworthy boundary loop (validate-by-ear → export → re-split), a fair stem-vs-stem
A/B for the lead single, and an edge-detection heuristic to build next.
---
log: 011
title: "The cpluck is the tell letting TF-IDF name a track"
date: 2026-06-05
task: "#34 locate-matrix, #35 38C3 PunkAChien, structuring"
tags: [tooling, salvage, fingerprint, pydantic, mastering]
shareable: true
---
## Cap (what & why)
After the ear caught a false "Hamburg PunkAChien" (log 010), we needed to stop
*guessing* which sample identifies a track and start *deriving* it — then use that to
find the real 38C3 PunkAChien, and finally structure the whole tidal→tracks toolchain
so it stops being a pile of scripts.
## Manœuvre (how)
- **Chased the 38C3 lead first** (faster than building UI): exported Take35 from the
Ardour session, fingerprinted its 13 orbits, summed to a 4:35 audition mix, served
it on the LAN for PLN's phone.
- **Built sample TF-IDF** over the local `.tidal` git corpus (772 docs). Each score =
a document, each Dirt-Samples folder = a term. The trick that made it precise:
vocabulary = `ls Dirt-Samples` (custom packs are symlinked in), and extract tokens
**only from sound-context strings** (`s`/`sound`/`#`), not `mask`/`n`/`range` — else
mininotation booleans like `"f"` collide with folder names and drown the signal.
- **Structured it with pydantic**: a shared `models.py` (Provenance, LocateCell,
MasterEDL, TfidfReport) — validate-on-write, JSON in/out.
- **Turned the ear-feedback into a typed EDL**: PLN's phone+WH-1000XM5 review became
9 `MasterEdit` rows.
## Prise (findings / artifacts)
- `tools/sample_tfidf.py``sample_tfidf.json` (289 KB, validated). PunkAChien's real
signature: **vec1_acid (df=18), cpluck (df=23)**; jungle_breaks/breaks165 are common
filler. `jungle_breaks` df=78, used everywhere — useless for ID, exactly as PLN said.
- `armada/tide-table/models.py` — the fleet's typed vocabulary.
- Take35 (2024-12-25, "Pitbul Punk") orbit fingerprint **matches PunkAChien** on the
track-specific signals (cpluck riff orbit-05, acid 04, 4:35 length). Pending ear.
- `master_edl_take89.json` — 9 edits, **3 of them `bad_cut` = ground-truth labels** for
the boundary detectors.
## Sel (the shareable learning)
A track's identity isn't its loudest sample — it's its *rarest* one. The kick is in
everything; the cpluck is in almost nothing. TF-IDF is just "how surprised am I to hear
this here," and surprise is identity. (Corollary, learned the hard way: silence isn't a
tell either — meth_bass goes quiet because the `d6` knob is awkward on the LaunchControl,
not because the song says so. The instrument leaks into the data.)
## Hameçon (hook)
"I asked a search algorithm which sound *is* my song. It ignored the drums and pointed
at one plucked cello note used in 23 tracks out of 800. It was right."
# tasks/ — L'Armada Captain's Log 🪵
**Document as we go.** Every achieved task gets a lean entry here. This salvage
operation is a *documentary in disguise* — these logs are the raw material for blog
posts, devlogs, and video narration later. Write the entry **when the task lands**,
while it's fresh.
## Rules
- One file per achieved task: `NNN-slug.md` (NNN = zero-padded sequence).
- **Lean > complete.** A few honest sentences beat a wall of text.
- Always capture the **shareable bit** — the insight or story beat a reader/viewer
would actually want. If there's nothing shareable, one line is fine.
- Numbers, links, and concrete artifacts > adjectives.
## Format
```markdown
---
log: 000
title: "Short punchy title"
date: YYYY-MM-DD
task: "#N board task (or n/a)"
tags: [distribution, salvage, tooling, ...]
shareable: true # blog/video-worthy?
---
## Cap (what & why)
One or two lines: what we set out to do, and why it mattered.
## Manœuvre (how)
The move — key decisions, approach, what we tried. Lean.
## Prise (findings / artifacts)
- Concrete outputs: files, numbers, links.
## Sel (the shareable learning) # 'sel' = the savor / the gold
- The insight worth telling. The thing future-you forgets.
## Hameçon (hook) # 'hameçon' = the hook, for content
A punchy angle / story beat for a future post or video.
## Sillage (what it unlocks) # 'sillage' = the wake left behind
What this makes possible next.
```
Sections are French nautical on purpose (sound ↔ sea). Drop any section that's empty.
# tide-table · *Table des Vagues*
The catalog — one source of truth for every ParVagues asset and where it lives.
## Sources (reconciled, not duplicated)
1. **`Prod/` archive** (freebox canonical) — masters, mixdowns, Audacity projects.
2. **Site setlists**`~/Work/Web/www/next/content/lives/**/tracks.json` already
names tracks per gig, with BPM, style, sample packs, and Tidal ingredients.
3. **External** — SoundCloud, Bandcamp, YouTube, archive.org, Spotify/DSPs, IG.
4. **Screen-rec / phone recordings** — provenance-tagged candidates.
5. **`backlog.md`** (repo root) — hand-kept **monthly creation log**: the chronological
spine. Dates tracks/études, reveals creation cadence, carries collab attributions
(`[Louis]`, `[Raph]`…) and status sections (WIP / ÉBAUCHES / Somewhat complete / Prod)
+ gig tracklists. Use it to date the undated SC/.tidal items and to seed the calendar.
## Source-fetch methods (learned the hard way)
- **Bandcamp** — WebFetch works: tracklist + **durations** + release date + license + tags.
- **Site** (Next.js SSR) — WebFetch works: gig, setlist, BPM/style, collabs. Note a gig may
have BOTH a **live recap** (`/parvagues/live/{slug}`) and an **owned-event landing**
(`/{slug}`) — capture both; the landing carries lineup/poster (→ flotille + media).
- **SoundCloud** — WebFetch BLOCKED (JS app, "browser not compatible"). Use **oEmbed**
(`https://soundcloud.com/oembed?format=json&url=…`) or the API instead.
- **Canonicalization** — reconcile FR⇄EN + stage-vs-release titles via `aka`
(e.g. *Blue Gold***L'Or Bleu**, *Sept1***Premier Septembre**). Track
`performed_not_released` so the gap is visible.
## Entry schema (per canonical track)
```yaml
- id: blue-gold # stable slug
title: "Blue Gold"
aka: ["L'Or Bleu"] # variants / French⇄EN
type: track # track | live-set | demo | sketch | remix | collab
key: null
bpm: null
genre: [breaks, ambient]
status: performed # written|demo|performed|recorded|mastered|released
tidal: [] # étude/ébauche source .tidal path(s) — the written genesis
collab: [] # flotille refs (see escales/flotille.yaml)
sources: # everywhere it exists
- { platform: prod, path: "Prod/...", format: flac, quality: master }
- { platform: soundcloud, url: "https://soundcloud.com/parvagues/..." }
- { platform: bandcamp, url: "..." }
- { platform: spotify, url: "..." } # presence = DSP-live ✓
- { platform: site, gig: "cosmicfest", role: setlist }
master: { path: null, lufs: null }
artwork: null
first_performed: null
release_date: null
notes: ""
```
## Gigs vs tracks — DO NOT conflate
A **gig / live-set** (CosmicFest, Opal, 38C3, CCC LIVE…) is one entry of
`type: live-set`. Its `tracks.json` *enumerates* the **tracks** (`type: track`)
performed in it. A gig name is **never** a track. The Prod `cosmicfest/` dir is the
*recording of that gig* → a source on the live-set entry, which `setlist:`-links to the
individual track entries it contains.
```yaml
- id: cosmicfest-2025
title: "Live @ CosmicFest v0"
type: live-set # NOT a track
sources: [{ platform: site, gig: cosmicfest }, { platform: prod, path: "Prod/cosmicfest/..." }]
setlist: [quand-on-decolle, fabuleux, technorage, blue-gold, ...] # → track entries
```
## Lifecycle: from étude to release
A track has a life **before** it's a track. The earliest state is **written** — a
`.tidal` *ébauche/étude*. Most études never progress, and that's fine: they're material
(seeds, studies, fragments), catalogued and searchable — not failures.
```
written (étude/ébauche) → demo → performed → recorded → mastered → released
```
The funnel as it stands (2026-06):
| stage | count |
|---|---|
| **written** (.tidal études) | ~818 — 688 `live/`, 86 `study/`, 12 `copycat/` … |
| performed | dozens (site setlists) |
| recorded (SoundCloud) | 68 |
| released (DSP) | 25 |
The site `tracks.json` `file` field already points each performed track to its `.tidal`
source → the scanner wires `released ← performed ← written` automatically. The full étude
corpus gets a lightweight index (`etudes.index`), so it's a seed bank, not dead weight.
## Sampling & rights tiers (releasability)
ParVagues' method is demucs-sampling, so DSP-releasability varies per track. Two fields:
- `sample_tier`: `original` | `light` | `heavy` | `remix` | `voice` (+ `set` for live sets)
- `rights`: `clear` | `review` | `needs-license` | `no-go`
Mapping → DSP decision:
| sample_tier | rights | DSP decision |
|---|---|---|
| original | clear | ✅ release |
| light | review | ✅ low-risk (or clear sample) |
| heavy | needs-license | 🟡 clear or swap sample before DSP |
| remix / cover | no-go | 🔴 SoundCloud / live-only unless licensed |
| voice / satire | no-go | 🔴 publicity/defamation risk (celebrity/political voice) |
Triage lives in `triage.csv` (auto-seeded; PLN classifies the judgment calls), then
gets ingested into per-track `sample_tier`/`rights` and a derived `dsp_ok`.
## Gap analysis (the payoff)
Once unified, query for **state mismatches** — e.g. tracks on Bandcamp/SoundCloud
but *absent from Spotify* (the "friend asks for new music on Spotify" problem).
Drives the `manifeste` release ordering.
## Build
`scan.py` (TODO) walks `Prod/` + ingests site `tracks.json` → emits `catalog.yaml`.
Katana-first: validate the scanner against a few known tracks before trusting it.
#!/usr/bin/env python3
"""
Ardour stem salvage — export per-take per-orbit stems from the single
"Tidal Multi" session into the Montreuil26 standard raw-stem layout:
Prod/_stems/{date}_{take}/orbit-NN.flac (48k stereo, lossless)
Prod/_stems/{date}_{take}/_manifest.json (take, date, orbits, sources)
Each Take = one performance. Sources live in the Ardour interchange dir,
named TakeN_<chan>-<region>%<L|R>.wav (chan = "Tidal 1".."Tidal 12"|Keys|Mic).
Mostly 1 region/orbit (direct L/R join); multi-region channels are concatenated
in region order first. SAFE: lossless, never touches the source session.
Usage:
ardour_stem_export.py --list # list takes (no audio)
ardour_stem_export.py --take Take3 --dry # show planned export
ardour_stem_export.py --take Take3 # export one take
ardour_stem_export.py --all # export all takes
"""
import xml.etree.ElementTree as ET
import argparse, collections, datetime, json, re, subprocess, sys
from pathlib import Path
ARDOUR = Path("/home/pln/Work/Sound/Ardour/Tidal Multi")
SESSION = ARDOUR / "Tidal Multi.ardour"
AUDIO = ARDOUR / "interchange" / "Tidal Multi" / "audiofiles"
OUT = Path("/mnt/freebox/PLN/Work/Sound/Prod/_stems")
NAME = re.compile(r'^(?P<take>.+?)_(?P<ch>Tidal \d+|Keys|Mic)-(?P<reg>\d+)(?:%(?P<lr>[LR]))?\.wav$')
def chan_key(ch):
m = re.match(r'Tidal (\d+)', ch)
return f"orbit-{int(m.group(1)):02d}" if m else ch.lower()
def parse():
root = ET.parse(SESSION).getroot()
sr = int(root.get('sample-rate') or 48000)
names = [s.get('name') for s in root.iter('Source') if s.get('name')]
# take -> chan -> lr('L'/'R'/'M') -> {reg:int -> filename}
takes = collections.defaultdict(lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
for nm in names:
m = NAME.match(nm)
if not m:
continue
lr = m.group('lr') or 'M'
takes[m.group('take')][m.group('ch')][lr][int(m.group('reg'))] = nm
return sr, takes
def ordered(d): # {reg:name} -> [name,...] by region
return [d[k] for k in sorted(d)]
def take_date(takes, take):
for ch in takes[take].values():
for lr in ch.values():
for nm in lr.values():
p = AUDIO / nm
if p.exists():
return datetime.date.fromtimestamp(p.stat().st_mtime).isoformat()
return "0000-00-00"
def concat_to(files, dst): # concat mono wavs (region order) -> one wav
if len(files) == 1:
return files[0]
inputs = []
for f in files: inputs += ["-i", str(AUDIO / f)]
fc = "".join(f"[{i}:a]" for i in range(len(files))) + f"concat=n={len(files)}:v=0:a=1[o]"
subprocess.run(["ffmpeg","-y","-loglevel","error",*inputs,"-filter_complex",fc,"-map","[o]",str(dst)],check=True)
return dst
def export_take(sr, takes, take, dry=False):
date = take_date(takes, take)
outdir = OUT / f"{date}_{take}"
kept = []
for ch in sorted(takes[take], key=chan_key):
slots = takes[take][ch]
Ls, Rs, Ms = ordered(slots.get('L',{})), ordered(slots.get('R',{})), ordered(slots.get('M',{}))
present = [f for f in (Ls+Rs+Ms) if (AUDIO/f).exists()]
if not present:
continue
out = outdir / f"{chan_key(ch)}.flac"
kept.append({"channel": ch, "out": out.name, "regions": max(len(Ls),len(Rs),len(Ms)),
"stereo": bool(Ls and Rs), "sources": present})
if dry:
continue
outdir.mkdir(parents=True, exist_ok=True)
tmp = []
if Ls and Rs: # stereo: join concatenated L + R
l = concat_to(Ls, outdir/"_L.wav"); r = concat_to(Rs, outdir/"_R.wav")
tmp += [p for p in (l,r) if isinstance(p,Path)]
subprocess.run(["ffmpeg","-y","-loglevel","error","-i",str(AUDIO/l if isinstance(l,str) else l),
"-i",str(AUDIO/r if isinstance(r,str) else r),
"-filter_complex","[0:a][1:a]join=inputs=2:channel_layout=stereo[a]",
"-map","[a]","-c:a","flac",str(out)],check=True)
else: # mono channel (Keys/Mic or single-side)
src = concat_to(Ms or Ls or Rs, outdir/"_M.wav")
subprocess.run(["ffmpeg","-y","-loglevel","error","-i",str(AUDIO/src if isinstance(src,str) else src),
"-c:a","flac",str(out)],check=True)
for t in tmp:
try: Path(t).unlink()
except OSError: pass
if not dry and kept:
(outdir/"_manifest.json").write_text(json.dumps(
{"take": take, "date": date, "sample_rate": sr,
"source": "Ardour 'Tidal Multi' interchange",
"orbits": [k for k in kept]}, indent=2, ensure_ascii=False))
return date, outdir, kept
if __name__ == "__main__":
import xml.etree.ElementTree as ET # noqa (kept local for clarity)
ap = argparse.ArgumentParser()
ap.add_argument("--take"); ap.add_argument("--all", action="store_true")
ap.add_argument("--list", action="store_true"); ap.add_argument("--dry", action="store_true")
a = ap.parse_args()
sr, takes = parse()
if a.list:
for tk in sorted(takes):
print(f"{take_date(takes,tk)} {tk:10} channels={len(takes[tk])}")
sys.exit(0)
targets = sorted(takes) if a.all else ([a.take] if a.take else [])
if not targets:
print("specify --take NAME | --all | --list"); sys.exit(1)
for tk in targets:
date, outdir, kept = export_take(sr, takes, tk, dry=a.dry)
tag = "DRY" if a.dry else "OK "
print(f"[{tag}] {tk} ({date}) -> {outdir} : {len(kept)} stems"
+ (f" e.g. {kept[0]['out']} <- {len(kept[0]['sources'])} src" if kept else ""))
{
"take": "Take89",
"date": "2026-05-22",
"gig": {
"set_title": "Montreuil Algorave V3 — Mai Floral",
"venue": "Les Nouveaux Sauvages, Montreuil, FR",
"src": "file:lives/2026/montreuil-algorave{.md,/tracks.json}",
"as_of": "2026-06-05"
},
"time_base": "proxy/stem seconds (= GT_master - 3; calibrated via locate, 09-split @2309.1)",
"sources_merged": [
"Downloads/take89_boundary_verdicts(1).json (2026-06-05 12:14, newer — wins on dedup)",
"Downloads/take89_boundary_verdicts.json (12:03, subset)",
"PLN chat message 2026-06-05 (detailed per-track ear notes)"
],
"boundaries": [
{"cut": 0, "into": "Piment brésilien", "t": 359, "mmss": "5:59", "verdict": "clean", "source": "export+ear", "note": "iconic g-funk 'piment' sample enters ~6:00"},
{"cut": 1, "into": "Take 5 Drops", "t": 725, "mmss": "12:05", "verdict": "clean", "source": "export(713)+ear", "note": "Piment outro + d10 ambient lingers 11:40→12:00; Take5 marimba/melodic by 12:10; PLN did a clean pure-percs transition. Export marked 713; ear ~12:05–12:10."},
{"cut": 2, "into": "Super Sunny Side Up", "t": 1133, "mmss": "18:53", "verdict": "clean", "source": "export(1131)+ear", "note": "Take5 outro ~18:35 with a riser overlap; Sunny in by 18:53"},
{"cut": 3, "into": "W.I.P. W.A.P.", "t": 1402, "mmss": "23:22", "verdict": "clean", "source": "export"},
{"cut": 4, "into": "'Plosive", "t": 1631, "mmss": "27:11", "verdict": "clean", "source": "export"},
{"cut": 5, "into": "Jeudi Drill", "t": 1822, "mmss": "30:22", "verdict": "trim_start", "source": "ear (CONFLICT)", "conflict": {"export_t": 1807.7, "message_t": 1822, "resolution": "1822", "why": "export was a -2s nudge; message is newer + explicit musical reason. Bad live transition: weird silence + hesitant first secs at GT 30:09; start clean at the marimba-riff entry 30:22. Drop 30:09–30:22."}},
{"cut": 6, "into": "Aria Sans Serif", "t": 2061, "mmss": "34:21", "verdict": "clean", "source": "export(2061.6)+ear AGREE"},
{"cut": 7, "into": "PunkAChien", "t": 2309, "mmss": "38:29", "verdict": "review", "source": "ear+analysis", "note": "cpluck riff carries across; kick+acid enter ~2330; d8 break dips at 2309; MFCC timbre shift ~2312–2316. PLN: riff 'suddenly steals' at 38:29, maybe a touch earlier. Single currently starts 2309.1. Precise sample-swap pin = timbral detector (#25). Candidate 2306–2309."},
{"cut": 8, "into": "You My Sunshine", "t": 2607, "mmss": "43:27", "verdict": "trim_start", "source": "ear", "note": "slight silence after PunkAChien; Sunshine real sound starts 43:26–27. PunkAChien single ends ~2600; trim the 2597–2607 gap for the Sunshine split."},
{"cut": 9, "into": "Desire", "t": 2926, "mmss": "48:46", "verdict": "trim_start", "source": "ear", "note": "trim a few secs of silence between last 'I know I know' linger (Sunshine) and Desire's synth loop start. GT 48:42; start where synth loop begins."},
{"cut": 10, "into": "L'or Bleu", "t": 3168, "mmss": "52:48", "verdict": "clean", "source": "export-gt"},
{"cut": 11, "into": "Premier Septembre", "t": 3426, "mmss": "57:06", "verdict": "clean", "source": "export-gt"},
{"cut": 12, "into": "Techno Orage", "t": 3754, "mmss": "62:34", "verdict": "trim_start+edit", "source": "ear", "note": "Sept1 chill pianos end ~62:20. d9 at 62:31 (~3751) is a MISTAKE → silence orbit-09 there. Start Orage at 62:34 on a nice fade-in of the orage drums+keys loops."},
{"cut": 13, "into": "La Révolution Sera Samplée", "t": 4173, "mmss": "69:33", "verdict": "trim_start+edit", "source": "ear", "note": "after Orage: silence, claps, encore — trim all for the release. Revolution starts after the claps/encore gap."}
],
"track_end_edits": {
"La Révolution Sera Samplée": "End on a BANG: ring out the last note of d10; fade d4 OUT a bit earlier so d4 isn't lingering when d10 rings; consider larger reverb (broader ring) + a little delay. → render as a FRAGMENT in a few versions to choose."
}
}
#!/usr/bin/env python3
"""Locate-Matrix layer 0 — the FREE metadata map (no audio). See locate-matrix.md.
Joins the canonical site tracklists × take_gig_map by DATE (±3d fallback) to produce
candidate `(track × take)` cells, an inverted track→takes index, and coverage stats.
Canonical track identity = the `.tidal` file path (so "Pitbul Punk" and "PunkAChien"
merge). Everything here is state=`candidate` (metadata prior) — nothing is `verified`
until the ear gate (L1). Honest about what didn't match.
"""
import json, re, glob
from datetime import date
from pathlib import Path
HERE = Path(__file__).parent
LIVES = Path("/home/pln/Work/Web/www/next/content/lives")
TAKE_MAP = HERE / "take_gig_map.md"
OUT = HERE / "track_recording_map.json"
AS_OF = "2026-06-05"
FUZZY_DAYS = 3
def load_gigs():
gigs = {}
for f in glob.glob(str(LIVES / "**/tracks.json"), recursive=True):
slug = str(Path(f).parent.relative_to(LIVES))
try:
d = json.load(open(f))
except Exception:
continue
tracks = d.get("tracks") if isinstance(d, dict) else d
dt = (d.get("date") if isinstance(d, dict) else None) or ""
gigs[slug] = {"date": dt[:10], "tracks": tracks or []}
return gigs
def parse_date(s):
m = re.match(r"(\d{4})-(\d{2})-(\d{2})", s or "")
return date(*map(int, m.groups())) if m else None
def load_takes():
takes = []
for line in TAKE_MAP.read_text().splitlines():
m = re.match(r"\|\s*(\d{4}-\d{2}-\d{2})\s*\|\s*(Take\d+)\s*\|\s*([\d:]+)\s*\|"
r"\s*(\d+)\s*\|\s*(\w+)\s*\|\s*(.*?)\s*\|", line)
if m:
d, tk, dur, orb, typ, label = m.groups()
takes.append({"date": d, "take": tk, "dur": dur, "orbits": int(orb),
"type": typ, "label": label})
return takes
def match_gig(take_date, gigs):
td = parse_date(take_date)
if not td:
return None, None
exact = [s for s, g in gigs.items() if g["date"] == take_date]
if len(exact) == 1:
return exact[0], "date-exact"
if len(exact) > 1:
return exact[0], "date-exact-AMBIGUOUS"
# ±FUZZY_DAYS window
near = sorted(
((abs((parse_date(g["date"]) - td).days), s)
for s, g in gigs.items() if parse_date(g["date"])),
key=lambda x: x[0],
)
if near and near[0][0] <= FUZZY_DAYS:
return near[0][1], f"date±{near[0][0]}d"
return None, None
def track_id(t):
"""Canonical id = .tidal path; fall back to name."""
f = (t.get("file") or "").strip()
return f or f"name:{t.get('name', '?')}"
def main():
gigs, takes = load_gigs(), load_takes()
cells, by_track = [], {}
matched = fuzzy = unmatched = 0
gig_has_take = set()
for tk in takes:
slug, method = match_gig(tk["date"], gigs)
if not slug:
unmatched += 1
continue
gig_has_take.add(slug)
if method == "date-exact":
matched += 1
else:
fuzzy += 1
is_set = tk["type"].upper() == "SET"
for tr in gigs[slug]["tracks"]:
tid = track_id(tr)
cell = {
"track": tid, "name": tr.get("name"), "tidal": tr.get("file"),
"take": tk["take"], "gig": slug, "take_type": tk["type"],
"bpm": tr.get("bpm"),
# SET take → track is somewhere in it; single-track take → ambiguous which
"state": "candidate" if is_set else "candidate-ambiguous",
"signals": {"metadata": True, "join": method},
"src": "derived:datejoin(site_tracklist×take_gig_map)", "as_of": AS_OF,
}
cells.append(cell)
rec = by_track.setdefault(tid, {"aliases": set(), "takes": set(), "gigs": set()})
if tr.get("name"):
rec["aliases"].add(tr["name"])
rec["takes"].add(tk["take"])
rec["gigs"].add(slug)
# release-readiness: tracks by # distinct takes that (per metadata) contain them
ranked = sorted(
({"track": k, "aliases": sorted(v["aliases"]), "n_takes": len(v["takes"]),
"takes": sorted(v["takes"]), "gigs": sorted(v["gigs"])}
for k, v in by_track.items()),
key=lambda x: -x["n_takes"],
)
out = {
"schema": "locate-matrix L0 (metadata prior; nothing verified)",
"as_of": AS_OF,
"stats": {
"takes_total": len(takes), "takes_matched_exact": matched,
"takes_matched_fuzzy": fuzzy, "takes_unmatched": unmatched,
"gigs_total": len(gigs), "gigs_with_take": len(gig_has_take),
"gigs_without_take": len(gigs) - len(gig_has_take),
"distinct_tracks": len(by_track),
"tracks_multi_take": sum(1 for r in ranked if r["n_takes"] >= 2),
"candidate_cells": len(cells),
},
"release_readiness": ranked,
"cells": cells,
}
OUT.write_text(json.dumps(out, ensure_ascii=False, indent=1))
s = out["stats"]
print(f"✓ {OUT}")
print(f" takes: {s['takes_total']} ({s['takes_matched_exact']} exact, "
f"{s['takes_matched_fuzzy']} ±{FUZZY_DAYS}d, {s['takes_unmatched']} unmatched)")
print(f" gigs: {s['gigs_with_take']}/{s['gigs_total']} have a take")
print(f" tracks: {s['distinct_tracks']} distinct, "
f"{s['tracks_multi_take']} in ≥2 takes (A/B/C possible)")
print(f" candidate cells: {s['candidate_cells']}")
print("\n top release-ready (most takes to choose from):")
for r in ranked[:12]:
nm = r["aliases"][0] if r["aliases"] else r["track"]
alias = f" «{', '.join(a for a in r['aliases'] if a != nm)}»" if len(r["aliases"]) > 1 else ""
print(f" {r['n_takes']:>2} takes {nm}{alias} → {', '.join(r['takes'])}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Reusable: build a keyboard-driven track-triage UI from the SoundCloud source cache.
Regenerate anytime: python3 build_triage_ui.py -> triage.html"""
import json, re, unicodedata
SRC="sources"
def norm(s):
s=(s or "").lower()
s=''.join(c for c in unicodedata.normalize('NFKD',s) if not unicodedata.combining(c))
s=re.sub(r'\(.*?\)','',s); s=re.sub(r'\[.*?\]','',s)
s=re.sub(r'(feat|ft|version|remix|instrumental|toma|take|wip|live).*','',s)
s=re.sub(r'[^a-z0-9]+',' ',s).strip()
return s
dsp=[r["trackName"] for r in json.load(open(f"{SRC}/itunes_songs.json"))["results"] if r.get("wrapperType")=="track"]
dsp+=[a["title"] for a in json.load(open(f"{SRC}/deezer_albums.json"))["data"]]
dn={norm(t) for t in dsp if norm(t)}
def on(t):
n=norm(t); return n in dn or any(n and (n in d or d in n) for d in dn)
REMIX=re.compile(r'\b(remix|cover|\bx\b|mason|princess superstar|cardi|wap)\b',re.I)
VOICE=re.compile(r'(macron|philippe|oudea|castera|cast[ée]ra)',re.I)
TEST =re.compile(r'(wip|toma uno|\bdemo\b|prototype|premi[eè]re prise|take five|beta|\btest\b)',re.I)
sc=[json.loads(l) for l in open(f"{SRC}/soundcloud.jsonl") if l.strip()]
tracks=[]
for t in sc:
title=t.get("title",""); dur=t.get("duration") or 0
if on(title): a="released"
elif dur>=720: a="set"
elif VOICE.search(title): a="voice"
elif REMIX.search(title): a="remix"
elif TEST.search(title): a="test"
else: a=""
tracks.append({"t":title,"u":t.get("webpage_url") or t.get("url") or "",
"y":(t.get("upload_date") or "")[:4] or "?","p":t.get("view_count") or 0,
"m":round(dur/60,1),"a":a})
# Triage UNIQUE tracks, not whole sets — sets are containers, split separately (see task #18).
SETS=[t for t in tracks if t["a"]=="set"]
tracks=[t for t in tracks if t["a"]!="set"]
tracks.sort(key=lambda x:-x["p"])
print(f"(excluded {len(SETS)} live-sets — they get split into tracks separately)")
TPL=r'''<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>L'Armada · Track Triage</title>
<style>
:root{--abyss:#06121c;--deep:#0b2233;--ink:#dff1f5;--mute:#8fb3c0;--line:#1d4358;--foam:#7fe3d4}
*{box-sizing:border-box}body{margin:0;font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;color:var(--ink);background:linear-gradient(180deg,var(--abyss),#02161f);height:100vh;overflow:hidden}
.app{display:grid;grid-template-columns:320px 1fr;height:100vh}
.side{border-right:1px solid var(--line);overflow:auto;padding:10px}
.side h1{font-size:14px;letter-spacing:1px;color:var(--foam);margin:6px 8px 4px}
.prog{font-size:12px;color:var(--mute);margin:0 8px 8px}
.bar{height:6px;background:#0a2c3c;border-radius:4px;margin:0 8px 10px;overflow:hidden}
.bar i{display:block;height:100%;background:var(--foam);transition:width .3s}
.row{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:7px;cursor:pointer;font-size:13px}
.row:hover{background:#0e2a3a}.row.cur{background:#12384c;outline:1px solid var(--foam)}
.dot{width:9px;height:9px;border-radius:50%;flex:0 0 auto;background:#26485a}
.row .ti{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}
.row .yr{color:var(--mute);font-size:11px}
.main{display:flex;flex-direction:column;padding:22px 26px;overflow:auto}
.crumbs{color:var(--mute);font-size:12px}
.title{font-size:26px;font-weight:700;margin:4px 0 2px}
.meta{color:var(--mute);font-size:13px;margin-bottom:12px}
.auto{display:inline-block;font-size:11px;border:1px solid var(--line);border-radius:6px;padding:1px 7px;color:var(--mute);margin-left:8px}
iframe{width:100%;height:120px;border:0;border-radius:10px;background:#0a2c3c;margin-bottom:16px}
.tiers{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px;max-width:760px}
.tier{display:flex;align-items:center;gap:10px;border:1px solid var(--line);border-radius:10px;padding:9px 11px;cursor:pointer;background:var(--deep);transition:.12s}
.tier:hover{border-color:var(--foam)}.tier.sel{outline:2px solid var(--foam);background:#103246}
.kbd{font-weight:800;width:22px;height:22px;border-radius:5px;display:grid;place-items:center;background:#0a2c3c;color:var(--ink);font-size:12px;flex:0 0 auto}
.tier .lab{font-size:13px}.tier .ds{font-size:11px;color:var(--mute)}
.note{margin-top:14px;max-width:760px}
.note input{width:100%;background:#0a2c3c;border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:9px 11px;font:inherit}
.hint{color:var(--mute);font-size:12px;margin-top:14px;max-width:760px}
.hint kbd{background:#0a2c3c;border:1px solid var(--line);border-radius:4px;padding:0 5px}
.tools{margin-top:auto;padding-top:16px;display:flex;gap:10px}
button{background:transparent;border:1px solid var(--line);color:var(--foam);border-radius:8px;padding:7px 13px;cursor:pointer;font:inherit}
button:hover{border-color:var(--foam)}
</style></head><body>
<div class="app">
<div class="side">
<h1>⛵ TRACK TRIAGE</h1>
<div class="prog" id="prog"></div><div class="bar"><i id="barfill"></i></div>
<div id="list"></div>
</div>
<div class="main">
<div class="crumbs" id="crumbs"></div>
<div class="title" id="title"></div>
<div class="meta" id="meta"></div>
<iframe id="player" allow="autoplay"></iframe>
<div class="tiers" id="tiers"></div>
<div class="note"><input id="note" placeholder="note (optional) — what's sampled, who's feat., etc."></div>
<div class="hint">Keys: <kbd>←</kbd><kbd>→</kbd> navigate · letter = tag &amp; advance · <kbd>Space</kbd> play/pause · <kbd>n</kbd> note · <kbd>Backspace</kbd> clear · auto-tags are dim until you confirm.</div>
<div class="tools">
<button onclick="expJSON()">⬇ Export JSON</button>
<button onclick="expCSV()">⬇ Export CSV</button>
<button onclick="if(confirm('Reset all annotations?')){localStorage.removeItem(KEY);location.reload()}">Reset</button>
</div>
</div>
</div>
<script src="https://w.soundcloud.com/player/api.js"></script>
<script>
const TRACKS=/*__DATA__*/;
const TIERS=[
{k:'o',id:'original',lab:'Original',c:'#46d39a',ds:'✅ release'},
{k:'l',id:'light',lab:'Light sampling',c:'#7fe3d4',ds:'✅ low-risk'},
{k:'h',id:'heavy',lab:'Heavy sampling',c:'#ffc24d',ds:'🟡 clear first'},
{k:'r',id:'remix',lab:'Remix / Cover',c:'#ff7a6b',ds:'🔴 live-only'},
{k:'v',id:'voice',lab:'Voice / Satire',c:'#ff6b6b',ds:'🔴 live-only'},
{k:'s',id:'set',lab:'Live-set',c:'#8fb3c0',ds:'🔴 live-only'},
{k:'t',id:'test',lab:'Test / Sketch',c:'#6a8290',ds:'— shelf'},
{k:'x',id:'released',lab:'Released',c:'#4a6b7a',ds:'skip'},
];
const TBK=Object.fromEntries(TIERS.map(t=>[t.k,t.id]));
const TBI=Object.fromEntries(TIERS.map(t=>[t.id,t]));
const KEY='armada_triage_v1';
let state=JSON.parse(localStorage.getItem(KEY)||'null');
if(!state){state={};TRACKS.forEach(t=>{if(t.a)state[t.u]={tier:t.a,note:'',auto:true};});}
let i=0,widget=null;
const $=id=>document.getElementById(id);
function save(){localStorage.setItem(KEY,JSON.stringify(state));}
function done(){return TRACKS.filter(t=>state[t.u]&&!state[t.u].auto).length;}
function loadPlayer(u){
const f=$('player');
f.src='https://w.soundcloud.com/player/?url='+encodeURIComponent(u)+'&color=%2339c2b0&auto_play=false&show_comments=false&visual=false&buying=false&sharing=false&download=false';
try{widget=SC.Widget(f);}catch(e){widget=null;}
}
function renderList(){
const L=$('list');L.innerHTML='';
TRACKS.forEach((t,n)=>{
const a=state[t.u];const col=a?TBI[a.tier].c:'#26485a';
const d=document.createElement('div');d.className='row'+(n===i?' cur':'');
d.innerHTML='<span class="dot" style="background:'+col+(a&&a.auto?';opacity:.45':'')+'"></span><span class="ti">'+t.t+'</span><span class="yr">'+t.y+'</span>';
d.onclick=()=>{i=n;render();};L.appendChild(d);
});
}
function render(){
const t=TRACKS[i];const a=state[t.u];
$('crumbs').textContent=(i+1)+' / '+TRACKS.length+(t.a?' · auto-guess: '+TBI[t.a].lab:'');
$('title').textContent=t.t;
$('meta').innerHTML=t.y+' · '+t.p+' plays · '+t.m+' min'+(a?' <span class="auto">'+(a.auto?'auto: ':'you: ')+TBI[a.tier].lab+'</span>':'');
const T=$('tiers');T.innerHTML='';
TIERS.forEach(tr=>{
const sel=a&&a.tier===tr.id;
const e=document.createElement('div');e.className='tier'+(sel?' sel':'');
e.style.borderLeft='4px solid '+tr.c;
e.innerHTML='<span class="kbd">'+tr.k.toUpperCase()+'</span><span><div class="lab">'+tr.lab+'</div><div class="ds">'+tr.ds+'</div></span>';
e.onclick=()=>tag(tr.id);T.appendChild(e);
});
$('note').value=a?a.note||'':'';
const d=done();$('prog').textContent=d+' / '+TRACKS.length+' confirmed ('+(TRACKS.length-d)+' to go)';
$('barfill').style.width=(100*d/TRACKS.length)+'%';
loadPlayer(t.u);renderList();
document.querySelector('.row.cur')?.scrollIntoView({block:'nearest'});
}
function tag(id){const t=TRACKS[i];state[t.u]={tier:id,note:($('note').value||''),auto:false};save();nextTodo();}
function nextTodo(){let j=i;for(let n=1;n<=TRACKS.length;n++){const k=(i+n)%TRACKS.length;const a=state[TRACKS[k].u];if(!a||a.auto){j=k;break;}}i=(j===i)?Math.min(i+1,TRACKS.length-1):j;render();}
function go(d){i=Math.max(0,Math.min(TRACKS.length-1,i+d));render();}
document.addEventListener('keydown',e=>{
if(e.target.id==='note'){if(e.key==='Enter'||e.key==='Escape'){state[TRACKS[i].u]=state[TRACKS[i].u]||{tier:'',auto:false};state[TRACKS[i].u].note=$('note').value;save();e.target.blur();}return;}
if(e.key==='ArrowRight'){go(1);e.preventDefault();}
else if(e.key==='ArrowLeft'){go(-1);e.preventDefault();}
else if(e.key===' '){if(widget)widget.toggle();e.preventDefault();}
else if(e.key==='n'){$('note').focus();e.preventDefault();}
else if(e.key==='Backspace'){delete state[TRACKS[i].u];save();render();e.preventDefault();}
else if(TBK[e.key.toLowerCase()]){tag(TBK[e.key.toLowerCase()]);e.preventDefault();}
});
function dl(name,txt,type){const b=new Blob([txt],{type});const u=URL.createObjectURL(b);const a=document.createElement('a');a.href=u;a.download=name;a.click();URL.revokeObjectURL(u);}
function expJSON(){const o=TRACKS.map(t=>({title:t.t,url:t.u,year:t.y,plays:t.p,min:t.m,tier:state[t.u]?.tier||'',dsp:state[t.u]?TBI[state[t.u].tier].ds:'',confirmed:state[t.u]?!state[t.u].auto:false,note:state[t.u]?.note||''}));dl('triage-export.json',JSON.stringify(o,null,2),'application/json');}
function expCSV(){const rows=[['title','url','tier','dsp','confirmed','note']];TRACKS.forEach(t=>{const a=state[t.u];rows.push([t.t,t.u,a?.tier||'',a?TBI[a.tier].ds:'',a?(!a.auto):'',a?.note||''].map(x=>'"'+String(x).replace(/"/g,'""')+'"'));});dl('triage-export.csv',rows.map(r=>r.join(',')).join('\n'),'text/csv');}
render();
</script></body></html>'''
open("triage.html","w").write(TPL.replace("/*__DATA__*/", json.dumps(tracks, ensure_ascii=False)))
print("triage.html written:", len(tracks), "tracks")
auto=sum(1 for t in tracks if t["a"])
print(f"pre-tagged (auto): {auto} | to classify by ear: {len(tracks)-auto}")
# tide-table · Table des Vagues — catalog (SEED / worked example)
# First real, reconciled data: CosmicFest v0, charted across site + Bandcamp + SoundCloud.
# Validates the schema & the performed-vs-released / FR⇄EN aliasing problem.
# status: idea | demo | performed | recorded | mastered | released
live_sets:
- id: cosmicfest-2025
title: "live@cosmicfest"
type: live-set
event: cosmicfest # ontology key
date: "2025-06-21" # performed (Labenne, FR)
released: "2025-06-28" # Bandcamp release
venue: "Labenne, France"
status: released
license: "CC BY-SA"
bpm_range: [80, 160]
collab: [rhadamanthe] # samples in Premier Septembre & Lady Perplexity
inspiration: ["Leifur James"]
sources:
- { platform: site, gig: cosmicfest, url: "https://me.nech.pl/parvagues/live/cosmicfest" }
- { platform: bandcamp, url: "https://parvagues.bandcamp.com/album/live-cosmicfest" }
- { platform: soundcloud, url: "https://soundcloud.com/parvagues/cosmicfest", note: "JS-rendered; fetch via oEmbed/API" }
- { platform: youtube, url: null, note: "2025 edition video exists on YouTube find URL" }
- { platform: prod, path: "Prod/cosmicfest/" }
setlist: [quand-on-decolle, fabuleux, paris, ghosts-in-the-toilets, nuit-dorage,
bain-electrique, la-fin-de-linsouciance, venons-ensemble, lor-bleu,
sunny-side-up, premier-septembre, lady-perplexity, lete-a-mauerpark, jeudi-drill]
performed_not_released: ["Ambient Cha0s??", "La C.r.e.m.e"] # in stage setlist, not on the release
notes: "Stage names differ from release titles see track aka fields."
tracks:
- { id: quand-on-decolle, title: "Quand on Décolle", type: track, bpm: 120, style: ambient, duration: "3:11", status: released }
- { id: fabuleux, title: "Fabuleux", type: track, bpm: 93, style: lounge, duration: "3:36", status: released }
- { id: paris, title: "Paris", type: track, bpm: 80, style: breaks, duration: "3:48", status: released }
- id: ghosts-in-the-toilets
title: "Ghosts in the T01l3ts"
aka: ["Ghosts in the Toilets"]
type: track
bpm: 160
style: dnb
duration: "6:20"
status: released
notes: "Texture from 38C3 (ghost sample)."
- id: nuit-dorage
title: "Nuit d'orage"
aka: ["TechnOrage"] # likely stage segment folded into the 23min storm piece — verify
type: track
bpm: 104
style: techno
duration: "23:19"
status: released
notes: "Long-form storm centrepiece (orage sample bank). Verify if it merges multiple stage cues."
- { id: bain-electrique, title: "Bain électrique", aka: ["Bain Electrique"], type: track, bpm: 128, style: breaks, duration: "3:58", status: released }
- id: la-fin-de-linsouciance
title: "La fin de l'insouciance"
aka: ["L'Insouciance", "insouciance"]
type: track
bpm: 120
style: techno
duration: "6:31"
status: released
- { id: venons-ensemble, title: "Venons Ensemble", type: track, bpm: 85, style: dnb, duration: "5:18", status: released, notes: "come_bass/come_eguitar/come_voice 3-part band pack." }
- id: lor-bleu
title: "L'Or Bleu"
aka: ["Blue Gold", "Blue Gold 🌇"] # FR release title ⇄ EN stage name
type: track
bpm: 94
style: lounge
duration: "5:17"
status: released
inspiration: ["Leifur James"]
notes: "suns_keys/suns_guitar/suns_voice originals."
- { id: sunny-side-up, title: "Sunny Side Up", type: track, bpm: 120, style: lounge, duration: "6:55", status: released }
- id: premier-septembre
title: "Premier Septembre"
aka: ["Sept1", "Sept 1"]
type: track
bpm: 120
style: collab
duration: "7:21"
status: released
collab: [rhadamanthe] # rhadamanthe_melo/vocal + cpluck
- id: lady-perplexity
title: "Lady Perplexity"
type: track
bpm: 138
style: breaks
duration: "4:51"
status: released
collab: [rhadamanthe]
- { id: lete-a-mauerpark, title: "L'été à Mauerpark", aka: ["Mauerpark"], type: track, bpm: 120, style: techno, duration: "3:40", status: released, notes: "moogBass Berlin warmth, Leslie/chorus." }
- { id: jeudi-drill, title: "Jeudi Drill", aka: ["JeuDrill"], type: track, bpm: 140, style: drill, duration: "4:40", status: released, notes: "bogdan_grime 'I'm from Cardiff!' vocals; marimba1 melody." }
# tide-table · Gap Report (corrected: DSP = Apple ∪ Deezer)
_Sources: SoundCloud (yt-dlp), Apple iTunes Search API, Deezer API. Apple is the richest DSP proxy; Deezer undercounts. Spotify pending (needs spotipy+creds)._
- **DSP catalog: 25 distinct titles**, last release **2024-10-04** (~frozen).
- **SoundCloud tracks: 51****10 already on DSP**, **41 not** (the backlog).
## Release backlog — SC tracks not on any DSP (triage: test / live-only / release)
| plays | year | min | title |
|--:|--:|--:|---|
| 121 | 2021 | 2.0 | I come from Cyberspace |
| 69 | 2021 | 1.0 | Perce-neige Printanier |
| 54 | 2025 | 11.0 | Live@38C3: Chaos Music Club 🪩 |
| 53 | 2022 | 2.0 | Faith in Bass (feat. Sculpture) |
| 48 | 2025 | 8.0 | Perfect Party - Mason X Princess Superstar live remix |
| 42 | 2020 | 5.0 | Samedi ? Interdit ! (feat. Macron) |
| 40 | 2020 | 3.0 | Soleil Pourpre |
| 37 | 2025 | 4.0 | WIP - WAP (Cardi B remix) |
| 31 | 2024 | 4.0 | El Mundo Interior (toma uno) |
| 30 | 2021 | 3.0 | Pixels Roses |
| 29 | 2021 | 2.0 | Jardin d'Hiver |
| 29 | 2021 | 6.0 | Tidal Crime Investigation |
| 29 | 2020 | 3.0 | Au revoir Lord Toyota |
| 27 | 2020 | 4.0 | Vie Hivernale |
| 25 | 2020 | 6.0 | Progressivement, Mardi |
| 23 | 2025 | 4.0 | Maudite soit la Guerre |
| 22 | 2020 | 4.0 | Stack Aérienne |
| 21 | 2025 | 7.0 | Premier Septembre (version longue) |
| 21 | 2021 | 2.0 | Un dimanche mineur |
| 19 | 2025 | 5.0 | Liquid Finale - feat @NVZT |
| 17 | 2020 | 5.0 | Ré comme remède |
| 17 | 2020 | 2.0 | Calorifère |
| 17 | 2020 | 3.0 | Du Code |
| 16 | 2025 | 6.0 | Transition à Fez : Ton numéro |
| 16 | 2025 | 4.0 | Ton Numéro |
| 15 | 2024 | 2.0 | Deck the (Dance) Hall |
| 14 | 2023 | 5.0 | Octobre Jaune |
| 14 | 2020 | 2.0 | Ciel Étoilé |
| 13 | 2023 | 4.0 | La Canopée |
| 13 | 2020 | 2.0 | Accel |
| 11 | 2020 | 3.0 | Battements Printaniers |
| 10 | 2023 | 5.0 | It's About Time |
| 9 | 2025 | 2.0 | Chez Cricri - Épisode 1 : Intro Citare |
| 9 | 2023 | 5.0 | Smart Motivated Person |
| 9 | 2020 | 4.0 | Dure Liberté |
| 8 | 2020 | 8.0 | Prière Dure |
| 7 | 2020 | 4.0 | Bleu Cassé |
| 7 | 2020 | 4.0 | YouMay |
| 5 | 2026 | 2.0 | Take Five Drops |
| 4 | 2026 | 2.0 | Piment Brésilien |
| 4 | 2020 | 4.0 | Pensif |
# L'Armada — Track × Recording Locate-Matrix (the fleet ledger)
> Goal: a **trustworthy map** of which catalog track lives in which recording, *where*
> (time-offset), and *how sure we are* — so we can pick best takes, form honest A/B/C
> comparisons, and mine sets (val-thorens, raise…) for releasable singles.
## Why this, why now
We tried to A/B PunkAChien and discovered we don't actually know what B is. The blind
cross-correlation that sourced "Hamburg PunkAChien" was a **false positive** (z=10 →
Insouciance→Liquid Finale). Empirical precision of acted-on locates so far = **1 TP / 1 FP
= 50%**. We have ~**1** ear-verified track↔take node and **0** systematic map. We can't
compare takes we haven't found. **Build the map before comparing anything.**
## The asymmetry that makes it tractable
We are NOT identifying tracks from nothing. We have strong priors:
- **23 canonical site tracklists** (raise 17, montreuil 15, val-thorens 14, cosmicfest 14…)
— what was played at each gig, with the `.tidal` file per track.
- **63 Ardour takes** mapped (loosely) to gigs (`take_gig_map.md`; 16 labeled, 37 blank).
- Each track's **`.tidal` score** (samples, `dN`→orbit map, `setcps` tempo).
- A growing set of **released references** (SoundCloud 51, DSP 25, Bandcamp) = real audio
of many catalog tracks.
So the job is mostly **"where in this take is the track the setlist says is there"** +
confidence, NOT blind search. Metadata collapses the hypothesis space; audio + ear confirm.
## Layered matcher (never blind xcorr again)
Each `(track, take)` cell accrues evidence from independent signals, then a **human gate**:
| layer | signal | strength | cost |
|---|---|---|---|
| **L0 metadata prior** | site tracklist × take→gig join | strong *whether*, zero *where* | free |
| **L1 ear gate (oracle)** | PLN confirms in the review UI | ground truth | human |
| **L2 cross-correlation** | track reference clip vs take onset-envelope → offset + z | strong *where* when a reference exists | cheap |
| **L3 tempo/structure** | `setcps` BPM + orbit-usage fingerprint vs take stem-map | disambiguates, no reference needed | medium |
Rules learned the hard way:
- **z-floor.** z=29 was right, z=10 was wrong → provisional accept floor **z ≥ 20**, and
**everything still goes to the ear** before `verified`. z is a ranking aid, not a verdict.
- **References bootstrap.** Released versions seed L2. The first ear-verified instance of a
track becomes the reference to find it in *other* takes (propagation).
- **Metadata ≠ truth either.** A tracklist may list a planned track that wasn't performed,
or a take→gig label may be `(±3d)` fuzzy. Priors are priors.
## Data model
`track_recording_map.json` — array of cells:
```jsonc
{
"track": "PunkAChien", // canonical track id
"tidal": "live/collab/raph/punkachien.tidal",
"take": "Take89", // recording id (Ardour take or .aup)
"gig": "2026/montreuil-algorave", // site slug (provenance, not a fact about audio)
"offset_s": 2309.1, "dur_s": 287.74,
"signals": { "metadata": true, "z": 29.0, "tempo_ok": true },
"state": "verified", // absent | candidate | suspect | verified
"src": "user:ear+derived:locate", // provenance type:locator
"as_of": "2026-06-05"
}
```
States: `candidate` (metadata says it should be here), `suspect` (audio hit but unconfirmed
/ low-z / conflicting), `verified` (ear-gated), `absent` (looked, not there).
## Output / payoff
- **Release-readiness by take-availability**: tracks appearing in ≥2 takes → A/B/C is
*possible*; rank singles by how many clean takes exist. (PunkAChien shows in 4 setlists.)
- **Set-mining**: val-thorens / raise tracklists → their takes → extractable singles.
- A **Bridge "fleet matrix" UI** (track rows × take cols, cell = state+z) to review and
ear-verify, reusing the design system.
## Build order (MVP-first)
- **L0 (this pass):** join site tracklists × `take_gig_map` → metadata candidate map +
inverted `track → [takes]` index + coverage stats. Pure data, zero audio. Honest about
unmatched gigs/takes.
- **L1:** minimal review UI — list candidates, play the take at the (eventually located)
offset, mark `verified|suspect|absent`. Ear is the oracle.
- **L2:** cross-correlation offset-finder (`tidal-ears master locate` already exists) wired
to references; fills `offset_s` + `z` for candidates; z≥20 + ear.
- **L3:** tempo + orbit fingerprint for reference-less disambiguation.
The misidentified `hamburg_stems` / `masters/hamburg_*` are kept as a salvage candidate
(Insouciance→Liquid Finale @ 39C3), not shipped as PunkAChien.
{
"take": "Take89",
"gig": "2026/montreuil-algorave",
"edits": [
{
"take": "Take89",
"track_no": 2,
"name": "Piment Brésilien",
"tidal": "live/collab/raph/piment_bresilien.tidal",
"op": "eq_highshelf",
"at": "whole",
"to": null,
"value": -2.0,
"kind": "harsh",
"reason": "highs piercing on mobile when lead/keys climb high under heavy FX",
"device": "WH-1000XM5+phone",
"status": "open",
"provenance": {
"source": "ear",
"locator": "aigus perçants sur mobile quand le lead/keys montent + FX forts — tame ~4-8kHz, dynamic",
"as_of": "2026-06-05",
"note": "phone + WH-1000XM5 resplit review"
}
},
{
"take": "Take89",
"track_no": 2,
"name": "Piment Brésilien → Take 5 Drops",
"tidal": null,
"op": "trim_out",
"at": "-5",
"to": null,
"value": null,
"kind": "bad_cut",
"reason": "boundary ~5s too early: next track's bass change bleeds into tail",
"device": "WH-1000XM5+phone",
"status": "open",
"provenance": {
"source": "ear",
"locator": "on entend le changement de basse de la suivante AVANT le cut",
"as_of": "2026-06-05",
"note": "phone + WH-1000XM5 resplit review"
}
},
{
"take": "Take89",
"track_no": 3,
"name": "Take 5 Drops → Super Sunny Side Up",
"tidal": "live/midi/nova/lounge/take_5_drops.tidal",
"op": "crossfade",
"at": "end",
"to": null,
"value": null,
"kind": "transition",
"reason": "hard silence→onset butt-join; add short crossfade or trim-to-groove",
"device": "WH-1000XM5+phone",
"status": "open",
"provenance": {
"source": "ear",
"locator": "silence à la fin de la 3 puis 4 démarre direct — abrupt",
"as_of": "2026-06-05",
"note": "phone + WH-1000XM5 resplit review"
}
},
{
"take": "Take89",
"track_no": 4,
"name": "Super Sunny Side Up",
"tidal": "live/midi/nova/lounge/sunny_side_up.tidal",
"op": "fade_out",
"at": "4:00",
"to": "4:09",
"value": null,
"kind": "laborious",
"reason": "outro laborious — end at 4:00, or 4:09 with a small fade",
"device": "WH-1000XM5+phone",
"status": "open",
"provenance": {
"source": "ear",
"locator": "se terminerait à 4:00, ou 4:09 sur petit fade",
"as_of": "2026-06-05",
"note": "phone + WH-1000XM5 resplit review"
}
},
{
"take": "Take89",
"track_no": 5,
"name": "W.I.P. W.A.P.",
"tidal": "live/midi/nova/dnb/wap.tidal",
"op": "trim_in",
"at": "1.0",
"to": null,
"value": null,
"kind": "bad_cut",
"reason": "trim ~1s break intro, start directly on the vocal",
"device": "WH-1000XM5+phone",
"status": "open",
"provenance": {
"source": "ear",
"locator": "commence sur 1s+ de break puis vocal — start au vocal",
"as_of": "2026-06-05",
"note": "phone + WH-1000XM5 resplit review"
}
},
{
"take": "Take89",
"track_no": 5,
"name": "W.I.P. W.A.P.",
"tidal": "live/midi/nova/dnb/wap.tidal",
"op": "investigate",
"at": "3:29",
"to": "3:31",
"value": null,
"kind": "anomaly",
"reason": "~2s odd silence/dropout — investigate (stem gap? mute glitch?)",
"device": "WH-1000XM5+phone",
"status": "open",
"provenance": {
"source": "ear",
"locator": "silence bizarre 3:29-3:31",
"as_of": "2026-06-05",
"note": "phone + WH-1000XM5 resplit review"
}
},
{
"take": "Take89",
"track_no": 5,
"name": "W.I.P. W.A.P. → 'Plosive",
"tidal": "live/midi/nova/dnb/wap.tidal",
"op": "crossfade",
"at": "3:26",
"to": "3:45",
"value": null,
"kind": "transition",
"reason": "messy tail 3:31-3:42; test fragment crossfades ~3:26→3:45 into 'Plosive",
"device": "WH-1000XM5+phone",
"status": "open",
"provenance": {
"source": "ear",
"locator": "bordel jusqu'à 3:42; crossfade direct 3:26→3:45+ ; PLN veut tester des fragments",
"as_of": "2026-06-05",
"note": "phone + WH-1000XM5 resplit review"
}
},
{
"take": "Take89",
"track_no": 6,
"name": "'Plosive",
"tidal": "live/midi/nova/dnb/plosive.tidal",
"op": "gain",
"at": "kick",
"to": null,
"value": -2.0,
"kind": "level",
"reason": "kick hotter than rest of set; match kick gain across tracks",
"device": "WH-1000XM5+phone",
"status": "open",
"provenance": {
"source": "ear",
"locator": "kick trop fort vs basses+mélodie; trop loud vs le kick des autres pistes — set-relative match",
"as_of": "2026-06-05",
"note": "phone + WH-1000XM5 resplit review"
}
},
{
"take": "Take89",
"track_no": 6,
"name": "'Plosive → Jeudi Drill",
"tidal": "live/midi/nova/dnb/plosive.tidal",
"op": "trim_out",
"at": "2:59",
"to": null,
"value": null,
"kind": "bad_cut",
"reason": "runs into T7 Jeudi Drill (Bogdan voice) — cut the next-track head",
"device": "WH-1000XM5+phone",
"status": "open",
"provenance": {
"source": "ear",
"locator": "2:59 'Plosive ends, next sound is Bogdan Jeudrill sample",
"as_of": "2026-06-05",
"note": "phone + WH-1000XM5 resplit review"
}
}
]
}
\ No newline at end of file
"""Shared pydantic models for the TidalCycles → tracks tooling (L'Armada).
One typed, JSON-serializable vocabulary across the fleet: provenance, the
locate-matrix cell, the master edit-decision list (EDL), and the sample TF-IDF
report. Every produced artifact is a model → validated on read, dumped on write.
Design rules encoded here:
- **Metadata carries provenance** (value + source TYPE + locator + as_of), so any
fact is traceable and corrections propagate. (feedback_metadata_provenance)
- **The ear is the oracle**: a match isn't `verified` until source=user/ear.
- **Never invent gig metadata**: gig is a site slug (a pointer), not a fact.
JSON: `m.model_dump_json(indent=1)` to write, `Model.model_validate_json(txt)` to
read. Enums serialize as their string value.
"""
from __future__ import annotations
from datetime import date
from enum import Enum
from typing import Optional
from pydantic import BaseModel, Field
# ── provenance ────────────────────────────────────────────────────────────────
class Source(str, Enum):
user = "user" # PLN said so (typed by hand)
ear = "ear" # PLN's ear on audio — the oracle for identity/mastering
file = "file" # read from a canonical file (site tracklist, .tidal…)
web = "web" # scraped/fetched online
derived = "derived" # computed (locate xcorr, tfidf, date-join…)
class Provenance(BaseModel):
"""Where a value came from, so it can be trusted and corrected."""
source: Source
locator: str = "" # path, URL, "xcorr z=29", "datejoin±3d"…
as_of: date
note: str = ""
# ── locate-matrix ─────────────────────────────────────────────────────────────
class MatchState(str, Enum):
absent = "absent" # looked, track not in this take
candidate = "candidate" # metadata says it should be here
candidate_ambiguous = "candidate-ambiguous" # single-track take, which track?
suspect = "suspect" # audio hit but low-z / conflicting
verified = "verified" # ear-gated truth
class LocateSignals(BaseModel):
metadata: bool = False
join: Optional[str] = None # "date-exact" | "date±3d" | "label"
z: Optional[float] = None # xcorr confidence (z ≥ 20 floor, ear still gates)
tempo_ok: Optional[bool] = None # setcps BPM matches
fingerprint: Optional[float] = None # IDF-weighted sample/orbit overlap (L3)
class LocateCell(BaseModel):
"""One (track × recording) cell of the fleet ledger."""
track: str # canonical id = .tidal path (merges aliases)
name: Optional[str] = None # display name in this gig ("Pitbul Punk")
tidal: Optional[str] = None
take: str # Ardour take or .aup id
gig: Optional[str] = None # site slug — a pointer, not an audio fact
take_type: Optional[str] = None # track | SET | sketch
bpm: Optional[float] = None
offset_s: Optional[float] = None
dur_s: Optional[float] = None
signals: LocateSignals = Field(default_factory=LocateSignals)
state: MatchState = MatchState.candidate
provenance: Optional[Provenance] = None
# ── master edit-decision list (EDL) — ear-feedback → typed fixes ──────────────
class EditOp(str, Enum):
trim_in = "trim_in" # cut N seconds off the head
trim_out = "trim_out" # cut the tail at `at`
fade_in = "fade_in"
fade_out = "fade_out"
crossfade = "crossfade" # `at`→`to` overlap into next track
gain = "gain" # level nudge (value = dB), e.g. kick match
eq_highshelf = "eq_highshelf" # tame highs (value = dB @ ~freq in note)
investigate = "investigate" # flagged anomaly, no op yet
class EditKind(str, Enum):
bad_cut = "bad_cut" # boundary bleed (also = ground-truth label!)
laborious = "laborious" # drags / outro too long
harsh = "harsh" # high-end piercing (esp. mobile)
level = "level" # cross-track loudness mismatch
anomaly = "anomaly" # dropout, weird silence
transition = "transition" # abrupt / needs crossfade
class EditStatus(str, Enum):
open = "open"
applied = "applied"
rejected = "rejected"
class MasterEdit(BaseModel):
"""A single mastering decision, sourced from the ear, replayable + labelable.
Each `bad_cut` is simultaneously a fix instruction AND a ground-truth label
for the boundary/edge detectors (#25/#31): tune them until they catch these.
"""
take: str = "Take89"
track_no: Optional[int] = None
name: str
tidal: Optional[str] = None
op: EditOp
at: str # timecode "m:ss" or seconds, within the track
to: Optional[str] = None # end timecode for crossfade/range ops
value: Optional[float] = None # dB for gain/eq, seconds for fades
kind: EditKind
reason: str
device: str = "" # "WH-1000XM5+phone" — translation context
status: EditStatus = EditStatus.open
provenance: Provenance
class MasterEDL(BaseModel):
take: str
gig: Optional[str] = None
edits: list[MasterEdit] = Field(default_factory=list)
# ── sample TF-IDF ─────────────────────────────────────────────────────────────
class SampleHit(BaseModel):
sample: str
score: float # tf-idf
df: int # document frequency across corpus
class TrackSignature(BaseModel):
n_samples: int
tf: dict[str, int]
top_tfidf: list[SampleHit] = Field(default_factory=list)
class TfidfReport(BaseModel):
corpus: str
n_docs: int
vocab_size: int
df: dict[str, int]
idf: dict[str, float]
tracks: dict[str, TrackSignature]
# Performance & recording notes — the archivist's log
Durable capture of **PLN's ear-feedback** on recordings, performances, samples,
transitions, mistakes and fixes. This is source material for better sampling,
effect choices, and auto-mastering heuristics — write down everything, even
small reactions. One section per take/performance; cite track + timecode.
> Convention (see project CLAUDE.md): every listening pass that yields a reaction
> gets logged here. Times are in the take's proxy/stem seconds unless noted.
---
## Take89 — Montreuil Algorave V3 "Mai Floral", 2026-05-22 (15 tracks)
> Gig metadata (venue **Les Nouveaux Sauvages, Montreuil**, org @a_f_alfl, visuals
> @shipow, poster, setlist) is canonical in `Work/Web/www/next/content/lives/2026/
> montreuil-algorave.md` + `/tracks.json` — **do not duplicate or invent here.**
> Mastering artifacts reference take-id + date only. (split_bandcamp.py's
> `ALBUM="…La Cour des Lézards"` is WRONG → fix in metadata quest.)
Boundaries (machine-readable, merged + conflict-resolved):
`tide-table/boundaries_take89_validated.json`. Calibration: proxy ≈ GT_master − 3.
### Listening pass 2026-06-05 — RESPLIT review, phone + Sony WH-1000XM5
> PLN auditioned ~half the Montreuil resplit masters on **mobile + WH-1000XM5**
> (phone playback = real mono/translation + small-driver test). Track numbers per
> canonical setlist (montreuil-algorave/tracks.json). Times = within-resplit-track.
> These drive #26 (resplit crossfade bleed), #31 (edge-detection), and a NEW
> cross-track-consistency concern (kick level, high-end harshness).
- **T2 Piment Brésilien***"stylé!"* BUT *"les aigus un peu trop perçants sur mobile
quand il monte vraiment high le piment comme les keys, surtout quand les effets sont
forts."* → **high-end harshness on mobile** when the lead/keys climb high + FX heavy.
Fix candidate: dynamic high-shelf / de-ess tied to FX intensity; check 4–8kHz on a
mobile-sim curve. (Tonal-balance instrument, mono/small-driver target.)
- **Boundary bleed (~T2→T3?)***"le début de la suivante mal cut genre 5s trop tôt?
On entend le changement de basse AVANT le cut!"* → split point ~5s too early; the
**next track's bass change is audible in the current track's tail**. Classic
crossfade/boundary bleed (#26). Likely Piment→Take5Drops; confirm by ear.
- **T3 Take 5 Drops → T4 Super Sunny Side Up***"transition un peu abrupte: silence à
la fin de la 3 puis 4 qui démarre direct?"* → **hard silence→onset**, no overlap.
Needs a short crossfade or trim-to-groove rather than dead-silence butt-join.
- **T4 Super Sunny Side Up***"se terminerait à 0400, ou à 0409 sur un petit fade —
moins laborieux la fin."* → **end is laborious**; trim to **4:00**, or **4:09 w/ a
small fade**. (Outro edge-detection #31.)
- **T5 W.I.P. W.A.P.**:
- *"commence sur 1s+ de break puis vocal? Commence direct au vocal voyons!"*
**trim the ~1s break intro, start on the vocal** (#31 intro-trim).
- *"silence bizarre de 0329 à 0331 — investigate?"***~2s odd silence/dropout @
3:29–3:31**; investigate source (gap in stems? mute glitch?).
- *"ensuite c'est le bordel jusqu'à 0342 où on commence à crossfade 'Plosive."*
messy tail 3:31–3:42. **Proposal: crossfade directly ~3:26 → 3:45+ into 'Plosive.**
PLN wants to **test fragments** of this transition. 😁
- **T6 'Plosive**:
- *"au casque Sony WH5, le kick est trop fort comparé aux basses et même aux éléments
mélodiques — ptet trop 'loud' comparé au mix du kick dans les autres pistes."*
**kick level inconsistent across the set** (Plosive kick hotter than others).
Cross-track kick-gain matching needed (set-relative mastering, not per-track only).
- *"2:59 'Plosive ends, next sound is Bogdan **Jeudrill** sample — bad cut."* → the
resplit of 'Plosive (T6) **runs into T7 Jeudi Drill** (jeudrill.tidal, Bogdan
sampled voice). Boundary ~5–10s late / includes next-track head (#26).
**Cross-cutting takeaways (feed the engines):**
1. **Boundaries are still bleeding both ways** — too-early (bass pre-echo) and too-late
(next-track head). Per-orbit timbral novelty (#25) + ear-gate, not level alone.
2. **High-end harshness is mobile-specific** → need a **mobile/mono translation check**
in the tonal instrument, not just LUFS/TP.
3. **Kick loudness must be matched ACROSS tracks** (set-relative), a new dimension on
top of per-track −14.
4. PLN wants a **fragment-test loop** for transitions (esp. WAP→'Plosive crossfade).
### Sample / sound identity (measured + PLN)
- **PunkAChien**: core riff = **cpluck** (orbit-05, A♯/G♯) octave-dropped — the
bass/riff spine, plays nearly throughout. **acid** = vec1_acid (orbit-04, D2),
front-half + a build. **moog** (orbit-09) = the deep mononote **sub**, drop-only
(enters ~242–281s of the single). `meth_bass` (orbit-06) **silent this take**
(named like a wobble monster, plays zero seconds — verify by ear, not by name).
- **Piment brésilien**: has an **iconic g-funk "piment" sample** (entry ~6:00) —
signature, recognisable. Outro carries a **d10 ambient** tail that lingers.
- **You My Sunshine**: built entirely on the **`no_sunshine`** sample ("Ain't No
Sunshine" — the "I know I know" vocal on d5, guitar/chops on d7/d9/d11).
- **Take 5 Drops**: marimba/melodic riff is the recognisable entry.
### Transitions & live mistakes (per PLN, by ear)
- **→ Jeudi Drill (30:09→30:22)**: *bad live transition* — "I changed tracks badly
this time": a weird silence then hesitant first seconds. Fix for release: start
clean at the **marimba-riff entry 30:22**, drop 30:09–30:22.
- **→ Aria (34:21)**: clean.
- **→ PunkAChien (38:29)**: riff "suddenly steals Aria's stage" — cpluck carries
across; kick+acid land ~38:50; perceived steal at 38:29 (= pattern re-trigger /
break dip). Wants maybe a touch earlier — precise pin pending timbral detection.
- **→ You My Sunshine (~43:27)**: slight silence after PunkAChien; real sound at
43:26–27 → trim the gap for the split.
- **→ Desire (~48:46)**: a few secs of silence between the last "I know I know"
linger and Desire's synth loop — trim it.
- **Premier Septembre → Techno Orage (62:20→62:34)**: Sept1 chill pianos end ~62:20;
**d9 at 62:31 is a MISTAKE → silence orbit-09 there, period**; start Orage at
**62:34** on a nice fade-in of the orage drums+keys loops.
- **→ La Révolution (69:33)**: preceded by silence, claps, **encore** — trim all
for the track release.
### Desired edits / fragment experiments
- **La Révolution — end on a BANG**: ring out the **last note of d10**; fade **d4
OUT earlier** so it isn't lingering when d10 rings; try **larger reverb** (broader
ring) + **a little delay**. → render a few **fragment** versions to choose.
- **Club master**: true −9 LUFS needs a limiter stage (current linear loudnorm caps
at ~−11.5 to respect −1 dBTP).
## How ParVagues performs (perspective for the whole system)
> Why this matters for tooling: a recorded take is **one live performance**, not a
> canonical arrangement. The same track sounds structurally different take-to-take
> because the *entry* is improvised to fit the set's momentum. Our analysis must not
> assume a fixed intro/section layout — detect it per-take.
- **The intro is a live transition decision.** Where each element spawns depends on
what came before and the floor energy. Examples of opening moves PLN uses:
- **spawn the bass early** when transitioning *onto a hot dancefloor* from a
previous track (skip the slow build — keep the body moving);
- **start ambient/atmos first** (e.g. lead with `d9`) for a slow, breathing open;
- **start on a drums transition** — e.g. HPF the incoming `jungle_breaks` and do a
`d8`-into-`d8` file switcharoo so the groove crossfades from the prior track.
- Consequence: "where does the track really start?" and section boundaries are
**performance-dependent**; the same single mastered from two takes will have
different lead-ins, builds, and drop placement. This is *why* the A/B players are
two **independent** transports (not a shared playhead) — and why intro/outro
edge-detection (#31) must be content-driven per take, never inferred from the score.
### Instrument / controller ergonomics (affects fingerprinting!)
- **`d6` (meth_bass) is sporadic, NOT track-specific.** PLN uses meth_bass *sometimes,
not always* — and its absence is partly a **controller-ergonomics artifact**: `d6` is
awkward to reach/trigger on the LaunchControl XL right now. So **orbit-06 silence is
not a discriminative fingerprint feature** — don't use it to identify a track; many
takes drop d6 for reasons that have nothing to do with the score. (Caught me
over-weighting "orbit-06 silent" as a PunkAChien tell, 2026-06-05.)
- **Long-run idea:** fix d6 ergonomics on the Tidal/MIDI side (remap so meth_bass is
reachable mid-flow) so its use reflects musical intent, not reach.
### Mastering reactions (PunkAChien)
- Montreuil reads **bass-forward** (centroid ~639 Hz, dom=sub) — confirm punchy not
boomy by ear. Montreuil is the **dynamic** take (LRA 7.4, has breakdown+drop).
- **38C3 take found = Take35** (2026-06-05): 2024-12-25, "Pitbul Punk", 4:35 standalone
track take. Fingerprint matches on *track-specific* signals: cpluck riff (orbit-05) +
acid (04) + percs + 4:35 length. **Pending PLN ear-verify** before `verified`.
- ⚠️ The old "Hamburg/Take87" PunkAChien was a **misidentification** (actually La Fin
de l'Insouciance → Liquid Finale @ 39C3). Do NOT A/B against it; real second take =
Take35 (38C3), plus Take36 (the 61:46 Toilet set) for an in-set version.
#!/usr/bin/env python3
"""PunkAChien stem-master builder (config-driven Rhadamanthe chain).
Katana first: track-level, role-driven master from per-orbit stems. Once proven
here, promote to `tidal-ears master track`. Reuses ffmpeg (no bespoke DSP).
Role map measured via `tidal-ears master eda` on Take89 [2387:2674.74]:
01 kick(A#1, low transient) 02 snare/clap 03 hats(air) 04 acid(D2, front half)
05 cpluck(A#2 — the riff/bass SPINE) 07 silent(disabled d7) 08 jungle break
09 moog SUB(drop only ~208-240s) 11 outro atmos 06/10/12 silent.
Chain (per MASTERING.md / layering.tidal):
per-stem gain-stage→HPF/LPF · bass bus (acid+cpluck+moog) sidechain-ducked by
kick · drums widened to sides, kick centred · sum · mono-bass ≤120 ·
glue comp 1.5:1 · two-pass loudnorm.
"""
import argparse, json, subprocess, sys, tempfile, re
from pathlib import Path
STEMS = Path(__file__).parent / "stems"
# role spec: orbit -> (label, highpass Hz, lowpass Hz|None, group)
# group ∈ {kick, drums, bass, top, atmos}; silent orbits omitted.
ROLES = {
"01": ("kick", 28, 18000, "kick"),
"02": ("snare", 160, None, "drums"),
"03": ("hats", 450, None, "drums"),
"04": ("acid", 70, None, "bass"),
"05": ("cpluck", 42, None, "bass"),
"08": ("break", 130, None, "top"),
"09": ("moog", 25, 1800, "bass"),
"11": ("atmos", 120, None, "atmos"),
}
def ff(args, **kw):
return subprocess.run(args, capture_output=True, text=True, **kw)
def loudnorm_measure(infile, I, TP, LRA):
"""Pass 1: measure."""
r = ff(["ffmpeg", "-hide_banner", "-nostats", "-i", str(infile),
"-af", f"loudnorm=I={I}:TP={TP}:LRA={LRA}:print_format=json",
"-f", "null", "-"])
m = re.findall(r"(\{[^{}]*?\"input_i\"[^{}]*?\})", r.stderr + r.stdout, re.S)
return json.loads(m[-1]) if m else None
def build(variant, target_lufs, sc_ratio, sc_thresh, widen, target_tp=-1.0,
fade_in=0.25, fade_out=4.0, dur=287.74, clip=None, moog_gain=0.0,
outdir="masters", prefix="montreuil"):
out = Path(__file__).parent / outdir / f"{prefix}_{variant}.flac"
seek = []
if clip:
cs, ce = (float(x) for x in clip.split(":"))
seek = ["-ss", str(cs), "-t", str(ce - cs)]
dur, fade_in, fade_out = ce - cs, 0.02, 0.15
inputs, idx = [], {}
for n, orbit in enumerate(ROLES):
f = STEMS / f"orbit-{orbit}.flac"
idx[orbit] = n
inputs += [*seek, "-i", str(f)]
parts, kick_lbl = [], None
bass_lbls, drum_lbls, top_lbls, atmos_lbls = [], [], [], []
for orbit, (label, hp, lp, group) in ROLES.items():
i = idx[orbit]
chain = [f"highpass=f={hp}"]
if lp:
chain.append(f"lowpass=f={lp}")
if group == "moog" or label == "moog":
chain.append("pan=stereo|c0=0.5*c0+0.5*c1|c1=0.5*c0+0.5*c1") # mono sub
if moog_gain:
chain.append(f"volume={moog_gain}dB")
lbl = f"s{orbit}"
parts.append(f"[{i}:a]" + ",".join(chain) + f"[{lbl}]")
if group == "kick":
kick_lbl = lbl
elif group == "bass":
bass_lbls.append(lbl)
elif group == "drums":
drum_lbls.append(lbl)
elif group == "top":
top_lbls.append(lbl)
elif group == "atmos":
atmos_lbls.append(lbl)
# kick split: one to mix, one as sidechain key
parts.append(f"[{kick_lbl}]asplit=2[kmix][kkey]")
# bass bus → sidechain-ducked by kick
parts.append("".join(f"[{l}]" for l in bass_lbls)
+ f"amix=inputs={len(bass_lbls)}:normalize=0[bassraw]")
if sc_ratio > 1.01:
sc_lin = 10 ** (sc_thresh / 20.0) # filter wants linear threshold
parts.append(f"[bassraw][kkey]sidechaincompress="
f"threshold={sc_lin:.6f}:ratio={sc_ratio}:attack=5:release=120:makeup=1[bass]")
else:
parts.append("[bassraw]anull[bass]")
# drums widened to the sides (Rhad: drums spatialised), kick stays centred
drum_mix = "".join(f"[{l}]" for l in drum_lbls) + \
f"amix=inputs={len(drum_lbls)}:normalize=0,extrastereo=m={widen}[drums]"
parts.append(drum_mix)
top_all = top_lbls + atmos_lbls
parts.append("".join(f"[{l}]" for l in top_all)
+ f"amix=inputs={len(top_all)}:normalize=0[top]")
# sum everything (normalize=0 keeps levels; we gain-staged via HPF only,
# so pre-trim the sum to leave headroom, then glue)
parts.append("[kmix][bass][drums][top]amix=inputs=4:normalize=0,"
"volume=-7dB[summed]")
# mono-bass ≤120: split, low→mono, recombine
parts.append("[summed]asplit=2[sumlo][sumhi]")
parts.append("[sumlo]lowpass=f=120,pan=stereo|c0=0.5*c0+0.5*c1|c1=0.5*c0+0.5*c1[lo]")
parts.append("[sumhi]highpass=f=120[hi]")
parts.append("[lo][hi]amix=inputs=2:normalize=0[mb]")
# glue comp
parts.append("[mb]acompressor=threshold=-22dB:ratio=1.5:attack=30:release=100:makeup=1dB[glue]")
# fades for clean intro/outro
parts.append(f"[glue]afade=t=in:st=0:d={fade_in},"
f"afade=t=out:st={dur-fade_out}:d={fade_out}[pre]")
graph = ";".join(parts)
tmp = Path(tempfile.gettempdir()) / f"pac_{variant}_pre.wav"
r = ff(["ffmpeg", "-hide_banner", "-nostats", *inputs,
"-filter_complex", graph, "-map", "[pre]",
"-c:a", "pcm_f32le", str(tmp), "-y"])
if r.returncode != 0:
print(r.stderr[-3000:], file=sys.stderr)
sys.exit(1)
# two-pass loudnorm
meas = loudnorm_measure(tmp, target_lufs, target_tp, 11)
ln = (f"loudnorm=I={target_lufs}:TP={target_tp}:LRA=11:"
f"measured_I={meas['input_i']}:measured_TP={meas['input_tp']}:"
f"measured_LRA={meas['input_lra']}:measured_thresh={meas['input_thresh']}:"
f"offset={meas['target_offset']}:linear=true:print_format=summary")
out.parent.mkdir(exist_ok=True)
r2 = ff(["ffmpeg", "-hide_banner", "-nostats", "-i", str(tmp),
"-af", ln, "-ar", "44100", "-c:a", "flac", "-compression_level", "8",
str(out), "-y"])
if r2.returncode != 0:
print(r2.stderr[-3000:], file=sys.stderr)
sys.exit(1)
tmp.unlink(missing_ok=True)
print(f"✓ {out.name} (LUFS≈{target_lufs}, sc_ratio={sc_ratio}, widen={widen})")
return out
def build_twotrack(src, variant, target_lufs, widen, target_tp=-1.0,
fade_in=0.0, fade_out=0.0):
"""Master a stereo 2-track (e.g. Hamburg live capture — no stems).
Bus-only chain: HPF/LPF → mono-bass ≤120 → drum-less widen → glue → loudnorm.
"""
out = Path(__file__).parent / "masters" / f"hamburg_{variant}.flac"
out.parent.mkdir(exist_ok=True)
dur = float(ff(["ffprobe", "-v", "error", "-show_entries", "format=duration",
"-of", "default=nw=1:nk=1", str(src)]).stdout.strip())
parts = [
"[0:a]highpass=f=22,lowpass=f=19000[clean]",
"[clean]asplit=2[lo0][hi0]",
"[lo0]lowpass=f=120,pan=stereo|c0=0.5*c0+0.5*c1|c1=0.5*c0+0.5*c1[lo]",
f"[hi0]highpass=f=120,extrastereo=m={widen}[hi]",
"[lo][hi]amix=inputs=2:normalize=0[mb]",
"[mb]acompressor=threshold=-22dB:ratio=1.5:attack=30:release=100:makeup=1dB[glue]",
]
last = "glue"
if fade_in or fade_out:
parts.append(f"[glue]afade=t=in:st=0:d={fade_in or 0.01},"
f"afade=t=out:st={dur-(fade_out or 0.01)}:d={fade_out or 0.01}[pre]")
last = "pre"
graph = ";".join(parts)
tmp = Path(tempfile.gettempdir()) / f"pac_ham_{variant}_pre.wav"
r = ff(["ffmpeg", "-hide_banner", "-nostats", "-i", str(src),
"-filter_complex", graph, "-map", f"[{last}]",
"-c:a", "pcm_f32le", str(tmp), "-y"])
if r.returncode != 0:
print(r.stderr[-3000:], file=sys.stderr); sys.exit(1)
meas = loudnorm_measure(tmp, target_lufs, target_tp, 11)
ln = (f"loudnorm=I={target_lufs}:TP={target_tp}:LRA=11:"
f"measured_I={meas['input_i']}:measured_TP={meas['input_tp']}:"
f"measured_LRA={meas['input_lra']}:measured_thresh={meas['input_thresh']}:"
f"offset={meas['target_offset']}:linear=true:print_format=summary")
r2 = ff(["ffmpeg", "-hide_banner", "-nostats", "-i", str(tmp), "-af", ln,
"-ar", "44100", "-c:a", "flac", "-compression_level", "8", str(out), "-y"])
if r2.returncode != 0:
print(r2.stderr[-3000:], file=sys.stderr); sys.exit(1)
tmp.unlink(missing_ok=True)
print(f"✓ {out.name} (LUFS≈{target_lufs}, widen={widen})")
return out
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("--twotrack", help="master a stereo file instead of stems")
ap.add_argument("--variant", required=True)
ap.add_argument("--lufs", type=float, default=-14.0)
ap.add_argument("--sc-ratio", type=float, default=3.0)
ap.add_argument("--sc-thresh", type=float, default=-24.0)
ap.add_argument("--widen", type=float, default=1.15)
ap.add_argument("--clip", help="render only START:END (seconds) → frags/ dir")
ap.add_argument("--moog-gain", type=float, default=0.0, help="dB trim on moog sub (orbit-09)")
ap.add_argument("--stems-dir", help="override stems dir (e.g. hamburg_stems)")
ap.add_argument("--dur", type=float, help="override track duration (s)")
ap.add_argument("--prefix", default="montreuil", help="output filename prefix")
a = ap.parse_args()
if a.stems_dir:
STEMS = Path(__file__).parent / a.stems_dir
if a.twotrack:
build_twotrack(a.twotrack, a.variant, a.lufs, a.widen)
else:
outdir = "frags" if a.clip else "masters"
kw = {"clip": a.clip, "moog_gain": a.moog_gain, "outdir": outdir, "prefix": a.prefix}
if a.dur: kw["dur"] = a.dur
build(a.variant, a.lufs, a.sc_ratio, a.sc_thresh, a.widen, **kw)
#!/usr/bin/env python3
"""Precompute the Judge UI's player data → armada/ui/public/punkachien.json.
The katana produces data; React consumes it. Per take we emit:
- orbit activity over time (RMS dB, 2 s bins), grouped by measured role family
- a momentary-LUFS trace per master (ffmpeg ebur128) + integrated LUFS / LRA / TP
Montreuil orbit activity comes from the windowed Take89 stem-map; Hamburg's is
computed from the already-windowed hamburg_stems. Calibration: stem ≈ master − 3.
"""
import json, re, subprocess, sys
from pathlib import Path
import numpy as np
HERE = Path(__file__).parent
OUT = HERE.parent.parent / "ui" / "public" / "punkachien.json"
DUR = 287.74
BIN_S = 2.0
ACTIVE_DB = -38.0 # lane lights above this
MONTREUIL_WIN = (2309.1, 2596.8)
# role families (DESIGN.md). vox folds into melodic's stack.
ROLE_GROUPS = [
{"key": "percs", "label": "Percs", "color": "#ff8c00", "glyph": "▰"},
{"key": "bass", "label": "Bass", "color": "#7c5cff", "glyph": "▂"},
{"key": "melodic", "label": "Melodic", "color": "#36c5f0", "glyph": "♪"},
{"key": "tops", "label": "Tops", "color": "#2dd4bf", "glyph": "≈"},
{"key": "atmos", "label": "Atmos", "color": "#8a93a6", "glyph": "◌"},
]
# measured PunkAChien arrangement (orbit -> label, family)
ROLE_MAP = {
"01": ("kick", "percs"), "02": ("snare", "percs"), "03": ("hats", "percs"),
"04": ("acid", "bass"), "05": ("cpluck", "bass"), "09": ("moog sub", "bass"),
"08": ("break", "tops"), "11": ("atmos", "atmos"),
}
def ff(args):
return subprocess.run(args, capture_output=True, text=True)
def ffprobe_dur(path):
r = ff(["ffprobe", "-v", "error", "-show_entries", "format=duration",
"-of", "default=nw=1:nk=1", str(path)])
try:
return float(r.stdout.strip())
except ValueError:
return DUR
def loudness(path):
"""Momentary-LUFS trace (0.5 s grid) + integrated / LRA / true-peak."""
r = ff(["ffmpeg", "-hide_banner", "-nostats", "-loglevel", "verbose",
"-i", str(path), "-filter_complex", "ebur128=peak=true",
"-f", "null", "-"])
txt = r.stderr
pts = re.findall(r"t:\s*([\d.]+).*?\sM:\s*(-?[\d.]+|-inf)", txt)
grid, cur, step = [], 0.0, 0.5
bucket = []
# frames ~ every 0.1s; bucket to 0.5s mean (ignoring -inf/silence floor)
for t, m in pts:
t = float(t)
val = None if m == "-inf" else float(m)
if t >= cur + step:
grid.append(round(float(np.mean([b for b in bucket if b is not None]
or [-70.0])), 1))
bucket = []
cur += step
bucket.append(val)
summ = txt.split("Summary:")[-1]
def g(pat, d):
m = re.search(pat, summ)
return round(float(m.group(1)), 1) if m else d
return {
"trace": grid, "stepS": step,
"I": g(r"I:\s*(-?[\d.]+)\s*LUFS", None),
"LRA": g(r"LRA:\s*(-?[\d.]+)\s*LU", None),
"TP": g(r"Peak:\s*(-?[\d.]+)\s*dBFS", None),
}
def montreuil_orbits(dur):
sm = json.loads((HERE / "stemmap_take89.json").read_text())
rms = sm["rms_db"]
sb = int(round(MONTREUIL_WIN[0] / BIN_S))
n = int(round(dur / BIN_S))
orbits = []
for i, name in enumerate(sm["orbits"]):
o = name.split("-")[1]
if o not in ROLE_MAP:
continue
act = [round(float(x), 1) for x in rms[i][sb:sb + n]]
if max(act) <= ACTIVE_DB:
continue
label, group = ROLE_MAP[o]
orbits.append({"orbit": o, "label": label, "group": group, "activity": act})
return orbits
def hamburg_orbits(dur):
stems = HERE / "hamburg_stems"
n = int(round(dur / BIN_S))
orbits = []
for o in (f"{i:02d}" for i in range(1, 13)):
if o not in ROLE_MAP:
continue
f = stems / f"orbit-{o}.flac"
if not f.exists():
continue
# decode mono @ 4kHz f32, RMS per 2s bin
r = subprocess.run(["ffmpeg", "-v", "error", "-i", str(f), "-ac", "1",
"-ar", "4000", "-f", "f32le", "-"], capture_output=True)
sig = np.frombuffer(r.stdout, dtype=np.float32)
sr = 4000
per = int(BIN_S * sr)
act = []
for b in range(n):
seg = sig[b * per:(b + 1) * per]
rms = float(np.sqrt(np.mean(seg**2))) if seg.size else 0.0
act.append(round(20 * np.log10(rms) if rms > 1e-7 else -240.0, 1))
if max(act) <= ACTIVE_DB:
continue
label, group = ROLE_MAP[o]
orbits.append({"orbit": o, "label": label, "group": group, "activity": act})
return orbits
def take(tid, label, gig, date, stream_f, club_f, orbits_fn):
dur = round(ffprobe_dur(HERE / stream_f), 2)
masters = {}
loud = {}
for k, fn in (("stream", stream_f), ("club", club_f)):
path = HERE / fn
ld = loudness(path)
masters[k] = {"file": fn, "I": ld["I"], "LRA": ld["LRA"], "TP": ld["TP"]}
loud[k] = {"trace": ld["trace"], "stepS": ld["stepS"]}
print(f" {tid}/{k}: I={ld['I']} LRA={ld['LRA']} TP={ld['TP']} "
f"({len(ld['trace'])} pts)", flush=True)
return {"id": tid, "label": label, "gig": gig, "date": date,
"dur": dur, "binS": BIN_S, "masters": masters, "loud": loud,
"orbits": orbits_fn(dur)}
def main():
print("building player data…", flush=True)
data = {
"track": "PunkAChien",
"calibration": "stem ≈ master − 3 s (verified by cross-correlation, z=29)",
"activeDb": ACTIVE_DB,
"roleGroups": ROLE_GROUPS,
# NOTE: the "Hamburg/39C3" take was MISIDENTIFIED — locate (z=10, weak) grabbed
# a La Fin de l'Insouciance → Liquid Finale stretch from Take87 (PunkAChien is
# not in the 39C3 set). Real Hamburg PunkAChien = 38C3 Toilet Rave 2024 ("Pitbul
# Punk", Take35/36); to be re-sourced + ear-verified before it returns to the A/B.
"note": "Single confirmed take (Montreuil). Hamburg/39C3 was misidentified "
"(actually Insouciance→Liquid Finale) and pulled; real Hamburg PunkAChien "
"= 38C3 Toilet 2024, pending re-source.",
"takes": [
take("montreuil", "Montreuil", "Montreuil Algorave V3 — Mai Floral",
"2026-05-22", "masters/montreuil_stream_v1.flac",
"masters/montreuil_club_v1.flac", montreuil_orbits),
],
}
OUT.parent.mkdir(parents=True, exist_ok=True)
OUT.write_text(json.dumps(data, ensure_ascii=False, separators=(",", ":")))
kb = OUT.stat().st_size / 1024
for t in data["takes"]:
print(f" {t['id']}: {len(t['orbits'])} active orbits "
f"({', '.join(o['label'] for o in t['orbits'])})")
print(f"✓ {OUT} ({kb:.0f} KB)")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Render stemmap_take89.json → interactive stem-map validation instrument.
Time ↓, orbits d1..d12 →, brightness = orbit RMS activity. Audio player (mp3
proxy of the summed stems, stem-time aligned) with click-to-seek, a moving
playhead, and BOUNDARY-STEPPING (jump to each boundary, hear 5s before→after)
so boundaries are validated by EAR, not memory. Track names labelled per segment.
"""
import json
from pathlib import Path
HERE = Path(__file__).parent
data = json.loads((HERE / "stemmap_take89.json").read_text())
# Boundaries in PROXY/stem time. CALIBRATED via `locate`: known-good
# 09-PunkAChien split sits at proxy 2309.1s (= GT-master 2312 − ~3). The proxy
# IS ~master time, so stem ≈ GT_master − 3 (NOT +75 — earlier error). Concretely
# GT_master − 78 maps the old (wrongly +75'd) array onto true proxy time.
GT_MASTER = [362, 716, 1134, 1405, 1634, 1812.71, 2056.74, 2312, 2599.74,
2925, 3171, 3429, 3742.65, 4176.14]
CAL = -3.0 # proxy = master + CAL (from locate: 2309.1 vs 2312)
GT_STEM = [round(g + CAL, 1) for g in GT_MASTER]
SETLIST = ["Quand on décolle", "Piment brésilien", "Take 5 Drops", "Super Sunny Side Up",
"W.I.P. W.A.P.", "'Plosive", "Jeudi Drill", "Aria Sans Serif", "PunkAChien",
"You My Sunshine", "Desire", "L'or Bleu", "Premier Septembre", "Techno Orage",
"La Révolution Sera Samplée"]
payload = {"orbits": data["orbits"], "bin_s": data["bin_s"], "times": data["times"],
"rms_db": data["rms_db"], "gt": GT_STEM, "setlist": SETLIST}
TEMPLATE = r"""<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Take89 Stem-Map — validate boundaries by ear</title>
<style>
body{margin:0;background:#0c0d11;color:#e6e4de;font:14px/1.5 -apple-system,Segoe UI,Roboto,sans-serif;}
header{padding:12px 16px;border-bottom:1px solid #232733;position:sticky;top:0;background:#0c0d11f2;z-index:9;backdrop-filter:blur(4px);}
h1{font-size:17px;margin:0 0 4px;} .dim{color:#8b8a85;font-size:12px;}
b.cyan{color:#46c8f6;} b.red{color:#ff5a78;} b.amber{color:#ffd166;}
.bar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:8px;}
button{background:#1b1e28;color:#e6e4de;border:1px solid #2c3140;border-radius:7px;padding:7px 11px;cursor:pointer;font-size:13px;}
button:hover{background:#262b38;} button.go{background:#46c8b6;color:#06231f;border:0;font-weight:700;}
#now{font:12px ui-monospace,monospace;color:#ffd166;min-width:150px;}
.ctl{display:flex;gap:12px;flex-wrap:wrap;align-items:center;margin-top:8px;font-size:11px;color:#9fb;}
.ctl input[type=range]{width:96px;vertical-align:middle;}
label.loop{color:#ffd166;}
.orbhdr{display:flex;margin-left:34px;font:11px ui-monospace,monospace;color:#9fb;margin-top:8px;}
.orbhdr span{text-align:center;}
.wrap{display:flex;align-items:flex-start;}
#ruler{width:50px;position:relative;font:11px ui-monospace,monospace;color:#8b8a85;}
#stage{position:relative;cursor:crosshair;}
canvas{display:block;image-rendering:pixelated;}
#overlay{position:absolute;left:0;top:0;right:0;bottom:0;pointer-events:none;}
.gtline{position:absolute;left:0;right:0;border-top:2px dashed #ff5a78;}
.gtline span{position:absolute;right:2px;top:-15px;font:10px ui-monospace;color:#ff5a78;background:#0c0d11cc;padding:0 3px;}
.detline{position:absolute;left:0;right:0;border-top:2px solid #46c8f6;opacity:.8;}
#playhead{position:absolute;left:0;right:0;border-top:2px solid #ffd166;display:none;}
.userline{position:absolute;left:0;right:0;border-top:2px solid #7CFC00;}
.userline span{position:absolute;left:2px;top:-15px;font:10px ui-monospace;color:#7CFC00;background:#0c0d11cc;padding:0 3px;}
#tracks{position:relative;width:230px;margin-left:8px;font-size:12px;}
.trk{position:absolute;left:0;right:0;border-left:3px solid #46c8b6;padding:1px 0 1px 8px;}
.trk b{color:#e6e4de;} .trk .t{color:#8b8a85;font:10px ui-monospace;}
.trk.cur{background:#173a35;} .trk:hover{background:#1b1e28;cursor:pointer;}
</style></head><body>
<header>
<h1>🛰️ Take89 stem-map — <span class="dim">validate every cut by ear</span></h1>
<div class="dim">time ↓ · orbits d1–d12 → · brightness = activity.
<b class="red">red dashed</b>=current boundaries (silence GT) · <b class="cyan">cyan</b>=auto-detected · <b class="amber">amber</b>=playhead.
Click the map to seek. Step boundaries to hear 5s before→after each cut.</div>
<div class="bar">
<button class="go" id=play>▶ play</button>
<button id=prev>◀ prev cut</button>
<button id=next>next cut ▶</button>
<span id=now>–:– / –:–</span>
<label>zoom <input id=zoom type=range min=1 max=16 step=1 value=2></label>
<span class=dim>(Space=play/pause · click map=seek)</span>
<label class="loop"><input type=checkbox id=loopck checked> loop ±<input id=pad type=number value=5 min=2 max=15 style="width:38px;background:#1b1e28;color:#ffd166;border:1px solid #2c3140;border-radius:4px"> s around cut</label>
<label>boundaries: <select id=bsrc><option value=gt>silence GT (validate these)</option><option value=det>auto-detected</option></select></label>
</div>
<div class="bar" id=valbar><span id=cutinfo class=dim>▸ step to a cut, hear it, then mark it</span>
<button id=expb>⬇ export verdicts</button></div>
<div class="ctl">auto-detect tuning:
<label>win <input id=win type=range min=4 max=40 step=2 value=16><span id=winv>16</span></label>
<label>present% <input id=pct type=range min=10 max=80 step=5 value=30><span id=pctv>30</span></label>
<label>active dB <input id=adb type=range min=-60 max=-30 step=1 value=-45><span id=adbv>-45</span></label>
<label>min trk s <input id=mt type=range min=20 max=180 step=5 value=70><span id=mtv>70</span></label>
<label>Δorbits <input id=ck type=range min=1 max=6 step=1 value=3><span id=ckv>3</span></label>
<span>detected: <b id=count>–</b> (set has 15 tracks)</span>
</div>
<div class="orbhdr" id=orbhdr></div>
</header>
<audio id=player src="proxy_take89.mp3" preload="auto"></audio>
<div class="wrap">
<div id=ruler></div>
<div id=stage><canvas id=cv></canvas><div id=overlay></div></div>
<div id=tracks></div>
</div>
<script>
const D = DATA_PLACEHOLDER, $=id=>document.getElementById(id);
const O=D.orbits.length, B=D.times.length, bin=D.bin_s, mat=D.rms_db;
const CW=46; let ROWH=2, H=B*ROWH;
const cv=$('cv'), ctx=cv.getContext('2d'); cv.width=O*CW;
$('orbhdr').innerHTML=D.orbits.map(n=>`<span style="width:${CW}px">d${+n.split('-').pop()}</span>`).join('');
const fmt=t=>{t=Math.max(0,t||0);const m=Math.floor(t/60),s=Math.floor(t%60);return m+':'+String(s).padStart(2,'0');};
const Y=t=>t/bin*ROWH; // time(s) → pixel y
// layout: (re)draw heatmap + ruler at current ROWH (zoom), then overlay
function layout(){
H=B*ROWH; cv.height=H;
$('overlay').style.height=H+'px'; $('ruler').style.height=H+'px'; $('tracks').style.height=H+'px';
for(let b=0;b<B;b++)for(let o=0;o<O;o++){let v=mat[o][b],x=Math.max(0,Math.min(1,(v+55)/43));
ctx.fillStyle=v<-100?'#070809':`rgb(${Math.round(x*60)},${Math.round(40+x*200)},${Math.round(30+x*90)})`;
ctx.fillRect(o*CW,b*ROWH,CW-1,Math.max(1,ROWH));}
const every=ROWH>=6?10:30; let rh='';
for(let b=0;b<B;b++){const t=D.times[b];if(Math.round(t)%every===0){
rh+=`<div style="position:absolute;top:${b*ROWH-6}px;right:5px">${fmt(t)}</div>`;}}
$('ruler').innerHTML=rh;
render();
}
// ---- boundary detection ----
function popcount(x){let c=0;while(x){c+=x&1;x>>=1;}return c;}
function detect(){const win=+$('win').value,pct=+$('pct').value/100,adb=+$('adb').value,mt=+$('mt').value,ck=+$('ck').value;
const bw=Math.max(1,Math.round(win/bin)),blocks=[];
for(let s=0;s<B;s+=bw){let m=0;for(let o=0;o<O;o++){let a=0,c=0;for(let i=s;i<Math.min(s+bw,B);i++){c++;if(mat[o][i]>adb)a++;}if(c&&a/c>=pct)m|=(1<<o);}blocks.push({t:D.times[s],m});}
const bd=[];let seg=blocks[0].m,last=-1e9;
for(let k=1;k<blocks.length-1;k++){if(popcount(seg^blocks[k].m)>=ck&&popcount(seg^blocks[k+1].m)>=ck&&blocks[k].t-last>=mt){bd.push(blocks[k].t);last=blocks[k].t;seg=blocks[k].m;}}
return bd;}
// ---- render overlay + track labels ----
let BOUNDS=[], CURSEG=-1;
function boundsList(){return $('bsrc').value==='gt'?D.gt.slice():detect();}
function render(){
['win','pct','adb','mt','ck'].forEach(id=>$(id+'v').textContent=$(id).value);
let html='';
// GT red (always shown for reference)
D.gt.forEach((t,i)=>{html+=`<div class=gtline style="top:${Y(t)}px"><span>GT ${i+2}</span></div>`;});
// detected cyan (when selected)
const det=detect();$('count').textContent=det.length;
if($('bsrc').value==='det')det.forEach(t=>{html+=`<div class=detline style="top:${Y(t)}px"></div>`;});
// user-validated green lines (verdict/nudge) — visible confirmation of your edits
Object.keys(VERD).forEach(k=>{const v=VERD[k];const t=v.t!=null?v.t:v.gt;if(t==null)return;
const tag=v.t!=null?`✎ ${fmt(t)}`:(v.verdict==='clean'?'✓':(v.verdict==='bleed'?'🚫':''));
html+=`<div class=userline style="top:${Y(t)}px"><span>${tag}</span></div>`;});
html+='<div id=playhead></div>';
$('overlay').innerHTML=html;
BOUNDS=boundsList();
// track labels in GT segments (authoritative names)
const segs=[0,...D.gt, D.times[B-1]];const tr=$('tracks');tr.innerHTML='';
for(let i=0;i<D.setlist.length;i++){const a=segs[i],b=segs[i+1]||D.times[B-1];
tr.innerHTML+=`<div class="trk" data-t="${a}" style="top:${Y(a)}px;height:${Y(b-a)}px">
<b>${i+1}. ${D.setlist[i]}</b><div class="t">${fmt(a)}–${fmt(b)}</div></div>`;}
[...document.querySelectorAll('.trk')].forEach(e=>e.onclick=()=>seek(+e.dataset.t+1));
}
['win','pct','adb','mt','ck'].forEach(id=>$(id).addEventListener('input',render));
$('bsrc').addEventListener('change',render);
$('zoom').addEventListener('input',()=>{const c=A.currentTime;ROWH=+$('zoom').value;layout();
window.scrollTo({top:$('stage').offsetTop+Y(c)-window.innerHeight*0.45});});
// Space = play/pause (not page scroll)
document.addEventListener('keydown',e=>{if(e.code==='Space'&&e.target.tagName!=='INPUT'&&e.target.tagName!=='SELECT'&&e.target.tagName!=='TEXTAREA'){e.preventDefault();$('play').click();}});
// ---- audio + playhead ----
const A=$('player');let loopEnd=null;
function seek(t){A.currentTime=Math.max(0,t);}
$('stage').addEventListener('click',e=>{const y=e.offsetY;seek(y/ROWH*bin);if(A.paused)A.play();});
$('play').onclick=()=>{loopEnd=null;if(A.paused){A.play();$('play').textContent='⏸ pause';}else{A.pause();$('play').textContent='▶ play';}};
const VERD=JSON.parse(localStorage.getItem('take89_bounds')||'{}');
function saveV(){localStorage.setItem('take89_bounds',JSON.stringify(VERD));}
function gotoCut(dir){const list=BOUNDS;if(!list.length)return;
const t=A.currentTime;let idx=list.findIndex(b=>b>t+0.3);
if(dir<0){const before=list.map((b,i)=>b<t-0.3?i:-1).filter(i=>i>=0);idx=before.length?before.pop():0;}
else if(idx<0)idx=list.length-1;
const b=list[idx];const pad=+$('pad').value;
CURSEG=idx; showCut(idx,b);
window.scrollTo({top:$('stage').offsetTop + b/bin*ROWH - window.innerHeight*0.45, behavior:'smooth'});
seek(b-pad); loopEnd=$('loopck').checked?b+pad:null; A.play();$('play').textContent='⏸ pause';}
$('next').onclick=()=>gotoCut(1); $('prev').onclick=()=>gotoCut(-1);
function showCut(idx,b){
const key=String(idx), v=VERD[key]||{};
const after=D.setlist[idx+1]||'?', before=D.setlist[idx]||'?';
const nudged=v.t!=null?` → ${fmt(v.t)}`:'';
$('cutinfo').innerHTML=`<b style="color:#46c8f6">cut ${idx+1}</b> @ ${fmt(b)}${nudged} `+
`<span class=dim>(${before} ▸ ${after})</span> &nbsp; `+
`<button onclick="mark('${key}','clean')">${v.verdict==='clean'?'✅':'✓'} clean</button>`+
`<button onclick="mark('${key}','bleed')">${v.verdict==='bleed'?'🚫':'🚫'} bleed</button>`+
`<button onclick="nudge('${key}',-2)">↤ −2s</button>`+
`<button onclick="nudge('${key}',2)">+2s ↦</button>`+
`<button onclick="setHere('${key}')">⌖ set = playhead</button>`;
if(v.verdict)$('cutinfo').style.color=v.verdict==='clean'?'#46c8b6':'#ff5a78';else $('cutinfo').style.color='';
}
window.mark=(k,verd)=>{VERD[k]={...(VERD[k]||{}),verdict:verd,gt:BOUNDS[+k]};saveV();showCut(+k,BOUNDS[+k]);};
window.nudge=(k,d)=>{const cur=(VERD[k]&&VERD[k].t!=null)?VERD[k].t:BOUNDS[+k];VERD[k]={...(VERD[k]||{}),t:Math.round((cur+d)*10)/10,gt:BOUNDS[+k]};saveV();seek(VERD[k].t-(+$('pad').value));A.play();showCut(+k,BOUNDS[+k]);};
window.setHere=(k)=>{VERD[k]={...(VERD[k]||{}),t:Math.round(A.currentTime*10)/10,gt:BOUNDS[+k]};saveV();showCut(+k,BOUNDS[+k]);};
$('expb').onclick=()=>{const blob=new Blob([JSON.stringify({proxy_time:true,verdicts:VERD},null,2)],{type:'application/json'});
const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='take89_boundary_verdicts.json';a.click();};
A.addEventListener('timeupdate',()=>{const ph=$('playhead');if(ph){ph.style.display='block';ph.style.top=(A.currentTime/bin*ROWH)+'px';}
$('now').textContent=`${fmt(A.currentTime)} / ${fmt(A.duration||0)}`;
if(loopEnd!=null&&A.currentTime>=loopEnd){A.pause();$('play').textContent='▶ play';loopEnd=null;}});
A.addEventListener('loadedmetadata',()=>{$('now').textContent=`0:00 / ${fmt(A.duration)}`;});
layout();
</script></body></html>"""
out = HERE / "stemmap.html"
out.write_text(TEMPLATE.replace("DATA_PLACEHOLDER", json.dumps(payload)))
print(f"Wrote {out} ({out.stat().st_size//1024} KB)")
{
"orbit-01.flac": {
"window": [
2470.0,
2510.0
],
"peak_dbfs": -2.7,
"rms_dbfs": -18.7,
"crest_db": 16.1,
"active_pct": 38.8,
"active_start_s": 6.7,
"active_end_s": 27.5,
"centroid_hz": 160,
"rolloff85_hz": 224,
"flatness": 0.0,
"dom_band": "sub",
"band_energy": {
"sub": 0.467,
"bass": 0.339,
"lowbass": 0.121,
"lowmid": 0.049,
"mid": 0.023,
"presence": 0.001,
"air": 0.0
},
"f0_median_hz": 58.6,
"note": "A\u266f1",
"voiced_pct": 32.8,
"role_guess": "kick / low-transient"
},
"orbit-02.flac": {
"window": [
2470.0,
2510.0
],
"peak_dbfs": -2.6,
"rms_dbfs": -28.3,
"crest_db": 25.7,
"active_pct": 23.0,
"active_start_s": 7.4,
"active_end_s": 28.8,
"centroid_hz": 3155,
"rolloff85_hz": 4947,
"flatness": 0.009,
"dom_band": "lowmid",
"band_energy": {
"sub": 0.014,
"bass": 0.04,
"lowbass": 0.341,
"lowmid": 0.398,
"mid": 0.055,
"presence": 0.077,
"air": 0.075
},
"f0_median_hz": null,
"note": null,
"voiced_pct": 0.0,
"role_guess": "mid element"
},
"orbit-03.flac": {
"window": [
2470.0,
2510.0
],
"peak_dbfs": -16.7,
"rms_dbfs": -43.6,
"crest_db": 26.9,
"active_pct": 38.3,
"active_start_s": 13.4,
"active_end_s": 28.7,
"centroid_hz": 8194,
"rolloff85_hz": 11804,
"flatness": 0.028,
"dom_band": "air",
"band_energy": {
"sub": 0.014,
"bass": 0.004,
"lowbass": 0.002,
"lowmid": 0.001,
"mid": 0.001,
"presence": 0.162,
"air": 0.81
},
"f0_median_hz": 235.9,
"note": "A\u266f3",
"voiced_pct": 34.6,
"role_guess": "hats / air / noise (high)"
},
"orbit-04.flac": {
"window": [
2470.0,
2510.0
],
"peak_dbfs": -15.2,
"rms_dbfs": -38.5,
"crest_db": 23.3,
"active_pct": 28.8,
"active_start_s": 7.3,
"active_end_s": 18.9,
"centroid_hz": 667,
"rolloff85_hz": 777,
"flatness": 0.0,
"dom_band": "mid",
"band_energy": {
"sub": 0.0,
"bass": 0.0,
"lowbass": 0.006,
"lowmid": 0.099,
"mid": 0.883,
"presence": 0.012,
"air": 0.0
},
"f0_median_hz": 72.6,
"note": "D2",
"voiced_pct": 24.7,
"role_guess": "mid element"
},
"orbit-05.flac": {
"window": [
2470.0,
2510.0
],
"peak_dbfs": -2.2,
"rms_dbfs": -20.4,
"crest_db": 18.2,
"active_pct": 94.4,
"active_start_s": 0.0,
"active_end_s": 40.0,
"centroid_hz": 516,
"rolloff85_hz": 671,
"flatness": 0.0,
"dom_band": "lowbass",
"band_energy": {
"sub": 0.0,
"bass": 0.005,
"lowbass": 0.43,
"lowmid": 0.367,
"mid": 0.196,
"presence": 0.002,
"air": 0.0
},
"f0_median_hz": 116.6,
"note": "A\u266f2",
"voiced_pct": 99.3,
"role_guess": "melodic / lead (tonal mid)"
},
"orbit-06.flac": {
"window": [
2470.0,
2510.0
],
"peak_dbfs": -240.0,
"rms_dbfs": -240.0,
"crest_db": 0.0,
"active_pct": 100.0,
"active_start_s": 0.0,
"active_end_s": 40.0,
"centroid_hz": 0,
"rolloff85_hz": 0,
"flatness": 1.0,
"dom_band": "sub",
"band_energy": {
"sub": 0.0,
"bass": 0.0,
"lowbass": 0.0,
"lowmid": 0.0,
"mid": 0.0,
"presence": 0.0,
"air": 0.0
},
"f0_median_hz": null,
"note": null,
"voiced_pct": 0.0,
"role_guess": "bass (low-dominant; check wobble/noise via flatness)"
},
"orbit-07.flac": {
"window": [
2470.0,
2510.0
],
"peak_dbfs": -53.6,
"rms_dbfs": -89.3,
"crest_db": 35.7,
"active_pct": 19.4,
"active_start_s": 0.0,
"active_end_s": 7.7,
"centroid_hz": 4509,
"rolloff85_hz": 7237,
"flatness": 0.014,
"dom_band": "presence",
"band_energy": {
"sub": 0.016,
"bass": 0.042,
"lowbass": 0.016,
"lowmid": 0.009,
"mid": 0.164,
"presence": 0.407,
"air": 0.342
},
"f0_median_hz": 123.5,
"note": "B2",
"voiced_pct": 18.1,
"role_guess": "perc / high-mid"
},
"orbit-08.flac": {
"window": [
2470.0,
2510.0
],
"peak_dbfs": -8.1,
"rms_dbfs": -32.5,
"crest_db": 24.4,
"active_pct": 58.1,
"active_start_s": 0.0,
"active_end_s": 40.0,
"centroid_hz": 2251,
"rolloff85_hz": 4270,
"flatness": 0.008,
"dom_band": "lowbass",
"band_energy": {
"sub": 0.05,
"bass": 0.265,
"lowbass": 0.285,
"lowmid": 0.074,
"mid": 0.173,
"presence": 0.111,
"air": 0.04
},
"f0_median_hz": null,
"note": null,
"voiced_pct": 0.0,
"role_guess": "mid element"
},
"orbit-09.flac": {
"window": [
2470.0,
2510.0
],
"peak_dbfs": -240.0,
"rms_dbfs": -240.0,
"crest_db": 0.0,
"active_pct": 100.0,
"active_start_s": 0.0,
"active_end_s": 40.0,
"centroid_hz": 0,
"rolloff85_hz": 0,
"flatness": 1.0,
"dom_band": "sub",
"band_energy": {
"sub": 0.0,
"bass": 0.0,
"lowbass": 0.0,
"lowmid": 0.0,
"mid": 0.0,
"presence": 0.0,
"air": 0.0
},
"f0_median_hz": null,
"note": null,
"voiced_pct": 0.0,
"role_guess": "bass (low-dominant; check wobble/noise via flatness)"
},
"orbit-10.flac": {
"window": [
2470.0,
2510.0
],
"peak_dbfs": -240.0,
"rms_dbfs": -240.0,
"crest_db": 0.0,
"active_pct": 100.0,
"active_start_s": 0.0,
"active_end_s": 40.0,
"centroid_hz": 0,
"rolloff85_hz": 0,
"flatness": 1.0,
"dom_band": "sub",
"band_energy": {
"sub": 0.0,
"bass": 0.0,
"lowbass": 0.0,
"lowmid": 0.0,
"mid": 0.0,
"presence": 0.0,
"air": 0.0
},
"f0_median_hz": null,
"note": null,
"voiced_pct": 0.0,
"role_guess": "bass (low-dominant; check wobble/noise via flatness)"
},
"orbit-11.flac": {
"window": [
2470.0,
2510.0
],
"peak_dbfs": -240.0,
"rms_dbfs": -240.0,
"crest_db": 0.0,
"active_pct": 100.0,
"active_start_s": 0.0,
"active_end_s": 40.0,
"centroid_hz": 0,
"rolloff85_hz": 0,
"flatness": 1.0,
"dom_band": "sub",
"band_energy": {
"sub": 0.0,
"bass": 0.0,
"lowbass": 0.0,
"lowmid": 0.0,
"mid": 0.0,
"presence": 0.0,
"air": 0.0
},
"f0_median_hz": null,
"note": null,
"voiced_pct": 0.0,
"role_guess": "bass (low-dominant; check wobble/noise via flatness)"
},
"orbit-12.flac": {
"window": [
2470.0,
2510.0
],
"peak_dbfs": -240.0,
"rms_dbfs": -240.0,
"crest_db": 0.0,
"active_pct": 100.0,
"active_start_s": 0.0,
"active_end_s": 40.0,
"centroid_hz": 0,
"rolloff85_hz": 0,
"flatness": 1.0,
"dom_band": "sub",
"band_energy": {
"sub": 0.0,
"bass": 0.0,
"lowbass": 0.0,
"lowmid": 0.0,
"mid": 0.0,
"presence": 0.0,
"air": 0.0
},
"f0_median_hz": null,
"note": null,
"voiced_pct": 0.0,
"role_guess": "bass (low-dominant; check wobble/noise via flatness)"
}
}
\ No newline at end of file
{
"orbit-01.flac": {
"window": [
2387.0,
2674.74
],
"peak_dbfs": 0.0,
"rms_dbfs": -15.3,
"crest_db": 15.3,
"active_pct": 46.5,
"active_start_s": 0.1,
"active_end_s": 285.1,
"centroid_hz": 151,
"rolloff85_hz": 208,
"flatness": 0.0,
"dom_band": "sub",
"band_energy": {
"sub": 0.514,
"bass": 0.323,
"lowbass": 0.12,
"lowmid": 0.028,
"mid": 0.013,
"presence": 0.001,
"air": 0.0
},
"f0_median_hz": 58.3,
"note": "A\u266f1",
"voiced_pct": 40.8,
"role_guess": "kick / low-transient"
},
"orbit-02.flac": {
"window": [
2387.0,
2674.74
],
"peak_dbfs": -2.4,
"rms_dbfs": -30.5,
"crest_db": 28.1,
"active_pct": 18.0,
"active_start_s": 0.1,
"active_end_s": 284.0,
"centroid_hz": 2913,
"rolloff85_hz": 4643,
"flatness": 0.008,
"dom_band": "lowmid",
"band_energy": {
"sub": 0.014,
"bass": 0.039,
"lowbass": 0.336,
"lowmid": 0.401,
"mid": 0.057,
"presence": 0.078,
"air": 0.075
},
"f0_median_hz": null,
"note": null,
"voiced_pct": 0.0,
"role_guess": "mid element"
},
"orbit-03.flac": {
"window": [
2387.0,
2674.74
],
"peak_dbfs": -14.7,
"rms_dbfs": -43.5,
"crest_db": 28.8,
"active_pct": 55.8,
"active_start_s": 0.0,
"active_end_s": 285.1,
"centroid_hz": 7834,
"rolloff85_hz": 11296,
"flatness": 0.035,
"dom_band": "air",
"band_energy": {
"sub": 0.013,
"bass": 0.003,
"lowbass": 0.002,
"lowmid": 0.001,
"mid": 0.005,
"presence": 0.17,
"air": 0.799
},
"f0_median_hz": 235.9,
"note": "A\u266f3",
"voiced_pct": 31.3,
"role_guess": "hats / air / noise (high)"
},
"orbit-04.flac": {
"window": [
2387.0,
2674.74
],
"peak_dbfs": -3.9,
"rms_dbfs": -28.7,
"crest_db": 24.8,
"active_pct": 35.4,
"active_start_s": 0.0,
"active_end_s": 169.8,
"centroid_hz": 641,
"rolloff85_hz": 776,
"flatness": 0.0,
"dom_band": "mid",
"band_energy": {
"sub": 0.0,
"bass": 0.018,
"lowbass": 0.014,
"lowmid": 0.12,
"mid": 0.836,
"presence": 0.012,
"air": 0.0
},
"f0_median_hz": 72.6,
"note": "D2",
"voiced_pct": 34.0,
"role_guess": "melodic / lead (tonal mid)"
},
"orbit-05.flac": {
"window": [
2387.0,
2674.74
],
"peak_dbfs": 0.0,
"rms_dbfs": -21.4,
"crest_db": 21.4,
"active_pct": 73.9,
"active_start_s": 0.0,
"active_end_s": 287.7,
"centroid_hz": 511,
"rolloff85_hz": 691,
"flatness": 0.0,
"dom_band": "lowmid",
"band_energy": {
"sub": 0.005,
"bass": 0.025,
"lowbass": 0.346,
"lowmid": 0.367,
"mid": 0.246,
"presence": 0.01,
"air": 0.0
},
"f0_median_hz": 208.9,
"note": "G\u266f3",
"voiced_pct": 80.2,
"role_guess": "melodic / lead (tonal mid)"
},
"orbit-06.flac": {
"window": [
2387.0,
2674.74
],
"peak_dbfs": -240.0,
"rms_dbfs": -240.0,
"crest_db": 0.0,
"active_pct": 100.0,
"active_start_s": 0.0,
"active_end_s": 287.7,
"centroid_hz": 0,
"rolloff85_hz": 0,
"flatness": 1.0,
"dom_band": "sub",
"band_energy": {
"sub": 0.0,
"bass": 0.0,
"lowbass": 0.0,
"lowmid": 0.0,
"mid": 0.0,
"presence": 0.0,
"air": 0.0
},
"f0_median_hz": null,
"note": null,
"voiced_pct": 0.0,
"role_guess": "bass (low-dominant; check wobble/noise via flatness)"
},
"orbit-07.flac": {
"window": [
2387.0,
2674.74
],
"peak_dbfs": -50.4,
"rms_dbfs": -79.9,
"crest_db": 29.5,
"active_pct": 29.8,
"active_start_s": 0.0,
"active_end_s": 90.7,
"centroid_hz": 2803,
"rolloff85_hz": 4633,
"flatness": 0.005,
"dom_band": "sub",
"band_energy": {
"sub": 0.621,
"bass": 0.128,
"lowbass": 0.021,
"lowmid": 0.007,
"mid": 0.029,
"presence": 0.065,
"air": 0.058
},
"f0_median_hz": 41.2,
"note": "E1",
"voiced_pct": 28.5,
"role_guess": "mid element"
},
"orbit-08.flac": {
"window": [
2387.0,
2674.74
],
"peak_dbfs": -3.8,
"rms_dbfs": -28.9,
"crest_db": 25.1,
"active_pct": 54.6,
"active_start_s": 0.0,
"active_end_s": 285.1,
"centroid_hz": 2037,
"rolloff85_hz": 3827,
"flatness": 0.006,
"dom_band": "bass",
"band_energy": {
"sub": 0.105,
"bass": 0.391,
"lowbass": 0.259,
"lowmid": 0.081,
"mid": 0.063,
"presence": 0.078,
"air": 0.02
},
"f0_median_hz": 31.2,
"note": "B0",
"voiced_pct": 3.5,
"role_guess": "mid element"
},
"orbit-09.flac": {
"window": [
2387.0,
2674.74
],
"peak_dbfs": 0.1,
"rms_dbfs": -16.7,
"crest_db": 16.7,
"active_pct": 13.3,
"active_start_s": 164.0,
"active_end_s": 203.6,
"centroid_hz": 116,
"rolloff85_hz": 165,
"flatness": 0.0,
"dom_band": "sub",
"band_energy": {
"sub": 0.42,
"bass": 0.381,
"lowbass": 0.147,
"lowmid": 0.029,
"mid": 0.018,
"presence": 0.0,
"air": 0.0
},
"f0_median_hz": 55.3,
"note": "A1",
"voiced_pct": 13.5,
"role_guess": "kick / low-transient"
},
"orbit-10.flac": {
"window": [
2387.0,
2674.74
],
"peak_dbfs": -240.0,
"rms_dbfs": -240.0,
"crest_db": 0.0,
"active_pct": 100.0,
"active_start_s": 0.0,
"active_end_s": 287.7,
"centroid_hz": 0,
"rolloff85_hz": 0,
"flatness": 1.0,
"dom_band": "sub",
"band_energy": {
"sub": 0.0,
"bass": 0.0,
"lowbass": 0.0,
"lowmid": 0.0,
"mid": 0.0,
"presence": 0.0,
"air": 0.0
},
"f0_median_hz": null,
"note": null,
"voiced_pct": 0.0,
"role_guess": "bass (low-dominant; check wobble/noise via flatness)"
},
"orbit-11.flac": {
"window": [
2387.0,
2674.74
],
"peak_dbfs": -17.0,
"rms_dbfs": -42.4,
"crest_db": 25.4,
"active_pct": 24.8,
"active_start_s": 216.4,
"active_end_s": 287.7,
"centroid_hz": 477,
"rolloff85_hz": 789,
"flatness": 0.0,
"dom_band": "lowmid",
"band_energy": {
"sub": 0.0,
"bass": 0.066,
"lowbass": 0.202,
"lowmid": 0.472,
"mid": 0.249,
"presence": 0.011,
"air": 0.0
},
"f0_median_hz": 65.8,
"note": "C2",
"voiced_pct": 20.3,
"role_guess": "mid element"
},
"orbit-12.flac": {
"window": [
2387.0,
2674.74
],
"peak_dbfs": -240.0,
"rms_dbfs": -240.0,
"crest_db": 0.0,
"active_pct": 100.0,
"active_start_s": 0.0,
"active_end_s": 287.7,
"centroid_hz": 0,
"rolloff85_hz": 0,
"flatness": 1.0,
"dom_band": "sub",
"band_energy": {
"sub": 0.0,
"bass": 0.0,
"lowbass": 0.0,
"lowmid": 0.0,
"mid": 0.0,
"presence": 0.0,
"air": 0.0
},
"f0_median_hz": null,
"note": null,
"voiced_pct": 0.0,
"role_guess": "bass (low-dominant; check wobble/noise via flatness)"
}
}
\ No newline at end of file
{
"ref": "39c3_HoT_master.flac",
"offset_s": 3199.11,
"confidence_z": 10.0,
"query_dur_s": 142.1
}
\ No newline at end of file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PunkAChien — Master A/B Judging</title>
<style>
:root { --bg:#0e0f13; --fg:#e8e6e0; --dim:#8b8a85; --acc:#e0506b; --acc2:#46c8b6;
--card:#171922; --line:#262a36; }
* { box-sizing:border-box; }
body { margin:0; font:15px/1.5 -apple-system,Segoe UI,Roboto,sans-serif;
background:var(--bg); color:var(--fg); padding:28px 22px 90px; }
h1 { font-size:23px; margin:0 0 4px; }
h2 { font-size:16px; letter-spacing:.04em; text-transform:uppercase; color:var(--acc2);
margin:34px 0 12px; border-bottom:1px solid var(--line); padding-bottom:6px; }
.sub { color:var(--dim); margin:0 0 6px; }
.brief { background:var(--card); border:1px solid var(--line); border-radius:10px;
padding:12px 16px; margin:16px 0; font-size:13.5px; color:#c9c7c0; }
code { background:#22252f; padding:1px 5px; border-radius:4px; font-size:12.5px; }
pre.map { background:#0a0b0e; border:1px solid var(--line); border-radius:8px;
padding:12px; overflow-x:auto; font-size:12px; color:#b7c4c0; }
.card { background:var(--card); border:1px solid var(--line); border-radius:10px;
padding:14px 16px; margin:10px 0; }
.row { display:flex; align-items:center; gap:12px; flex-wrap:wrap; }
.ttl { font-weight:600; }
.stats { color:var(--dim); font-size:12px; font-family:ui-monospace,monospace; }
button.play { background:var(--acc); color:#fff; border:0; border-radius:7px;
padding:8px 14px; cursor:pointer; font-weight:600; font-size:14px; }
button.play.playing { background:var(--acc2); color:#06231f; }
.q { margin-top:6px; }
.opts label { display:inline-flex; align-items:center; gap:6px; margin-right:18px;
cursor:pointer; }
.ab { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
.ab .card { margin:0; }
textarea { width:100%; background:#0a0b0e; color:var(--fg); border:1px solid var(--line);
border-radius:7px; padding:8px; font:13px/1.4 inherit; margin-top:8px; resize:vertical; }
.pill { font-size:11px; padding:2px 8px; border-radius:20px; background:#22252f; color:var(--dim); }
.barwrap { flex:1; min-width:160px; height:6px; background:#22252f; border-radius:3px; overflow:hidden; }
.bar { height:100%; width:0; background:var(--acc2); }
footer { position:fixed; bottom:0; left:0; right:0; background:#0a0b0e;
border-top:1px solid var(--line); padding:12px 22px; display:flex; gap:14px; align-items:center; }
button.exp { background:var(--acc2); color:#06231f; border:0; border-radius:7px;
padding:9px 16px; font-weight:700; cursor:pointer; }
.saved { color:var(--dim); font-size:12px; }
.hl { color:var(--acc); }
</style>
</head>
<body>
<h1>🐕 PunkAChien — Master A/B <span class="hl">fresh-ears judging</span></h1>
<p class="sub">Lead relaunch single. <b>A = Montreuil</b> (2026-05-22, stem-master from 12 orbits) ·
<b>B = Live@Hamburg</b> (39C3, 2-track capture). All clips loudness-matched to −14 LUFS for fair comparison.</p>
<div class="brief">
<b>How to use:</b> play each, pick what wins, add notes. Your calls export to JSON (button bottom-right) —
that drives the next master pass. Listen on monitors <i>and</i> phone speaker if you can.
The <b>fragments</b> isolate the specific choices my ear couldn't settle.
</div>
<h2>The track, measured (EDA on the stems)</h2>
<pre class="map">PunkAChien = techno × jungle "punk", tonal centre ~D minor / A♯. 0 ───────────────────────────── 287s
orbit-05 cpluck (A♯2) ── the riff / bass SPINE, plays almost throughout ─────────────────────────────
orbit-01 kick ████ strong, drops out in the breakdown (~140-168s), slams back ───────────────── ████
orbit-04 acid (D2) front half + breakdown build, then GONE ──────────┘
orbit-08 jungle break intermittent drive, strongest in the OUTRO ─────────────────────────── ▆▆▆▆
orbit-09 moog SUB silent until ~208s → enters HUGE for the DROP ────────────────── ███ ──┘
orbit-11 atmos outro texture only ───────────────────────────────────────────────────── ▂▂▂
orbit-06 meth_bass = SILENT this take · orbit-07 = disabled · 10/12 = unused
Sections: INTRO/BUILD 0-36 · GROOVE 36-140 · BREAKDOWN 140-168 · RE-ENTRY 168-208 · DROP 208-240 · OUTRO 240-287</pre>
<h2>① The 4 full masters</h2>
<div id="masters"></div>
<div class="card">
<div class="ttl">Overall verdict</div>
<div class="q opts">Which <b>take</b> ships as the single?
<span data-q="take"></span>
</div>
<div class="q opts">Streaming loudness feels…
<span data-q="loudness"></span>
</div>
<textarea data-note="overall" rows="2" placeholder="Overall notes: low-end balance? does the drop hit? Montreuil dynamics vs Hamburg energy?"></textarea>
</div>
<h2>② Fragments — the calls I couldn't make</h2>
<div id="frags"></div>
<footer>
<button class="exp" onclick="exportJSON()">⬇ Export verdicts JSON</button>
<span class="saved" id="saved">autosaving to this browser…</span>
</footer>
<script>
const MASTERS = [
{id:"m_mtl_stream", file:"masters/montreuil_stream_v1.flac", ttl:"A · Montreuil — streaming",
stats:"−14.0 LUFS · TP −0.9 · LRA 7.4 · 4:47 · stem-master"},
{id:"m_mtl_club", file:"masters/montreuil_club_v1.flac", ttl:"A · Montreuil — club/loud",
stats:"−11.5 LUFS · TP −0.9 · LRA 4.0 · (true −9 needs limiter stage)"},
{id:"m_ham_stream", file:"masters/hamburg_stem_stream_v1.flac", ttl:"B · Hamburg (39C3 House of Tea) — streaming",
stats:"−13.9 LUFS · TP −1.0 · LRA 4.0 · 2:22 · STEM-master (Take87)"},
{id:"m_ham_club", file:"masters/hamburg_stem_club_v1.flac", ttl:"B · Hamburg — club/loud",
stats:"−9 target · STEM-master (Take87, House of Tea, 2025-12-28)"},
];
const FRAGS = [
{q:"Sidechain depth (groove ~2:00-2:24) — how much pump suits a punk/jungle track?",
key:"sidechain",
a:{file:"frags/montreuil_frag_sc_gentle.flac", label:"Gentle (ratio 2)"},
b:{file:"frags/montreuil_frag_sc_strong.flac", label:"Strong / pumping (ratio 6)"}},
{q:"The DROP (~4:04-4:34, moog sub) — how loud should the moog SUB sit?",
key:"drop_sub",
a:{file:"frags/montreuil_frag_drop_v1.flac", label:"As-is (balanced)"},
b:{file:"frags/montreuil_frag_drop_subfwd.flac", label:"Sub-forward (+4dB, deeper sidechain)"}},
{q:"Take feel — same energy moment, which performance carries it?",
key:"take_feel",
a:{file:"frags/montreuil_frag_drop_v1.flac", label:"A · Montreuil (the drop)"},
b:{file:"frags/hamburg_groove.flac", label:"B · Hamburg (groove 60-84s)"}},
];
const INTRO = {file:"frags/montreuil_frag_intro.flac", q:"Does the opening (0-22s) work, or want a longer build / different start?"};
const store = JSON.parse(localStorage.getItem("pac_judge")||"{}");
function save(){ localStorage.setItem("pac_judge", JSON.stringify(store));
document.getElementById("saved").textContent = "saved ✓ "+new Date().toLocaleTimeString(); }
let current=null;
function mkPlayer(file){
const a=new Audio(file); a.preload="none";
const btn=document.createElement("button"); btn.className="play"; btn.textContent="▶";
const bw=document.createElement("div"); bw.className="barwrap";
const bar=document.createElement("div"); bar.className="bar"; bw.appendChild(bar);
btn.onclick=()=>{ if(!a.paused){a.pause();return;}
if(current&&current!==a){current.pause();} current=a; a.play(); };
a.onplay=()=>{btn.classList.add("playing");btn.textContent="⏸";};
a.onpause=()=>{btn.classList.remove("playing");btn.textContent="▶";};
a.onended=()=>{btn.classList.remove("playing");btn.textContent="▶";bar.style.width="0";};
a.ontimeupdate=()=>{bar.style.width=(100*a.currentTime/(a.duration||1))+"%";};
return {btn,bw};
}
function radios(span, key, opts){
opts.forEach(o=>{ const l=document.createElement("label");
const r=document.createElement("input"); r.type="radio"; r.name=key; r.value=o;
if(store[key]===o) r.checked=true;
r.onchange=()=>{store[key]=o;save();};
l.appendChild(r); l.appendChild(document.createTextNode(o)); span.appendChild(l); });
}
// masters
const mc=document.getElementById("masters");
MASTERS.forEach(m=>{ const c=document.createElement("div"); c.className="card";
const row=document.createElement("div"); row.className="row";
const p=mkPlayer(m.file);
const t=document.createElement("div"); t.innerHTML=`<div class="ttl">${m.ttl}</div><div class="stats">${m.stats}</div>`;
row.appendChild(p.btn); row.appendChild(t); row.appendChild(p.bw);
c.appendChild(row); mc.appendChild(c); });
document.querySelector('[data-q="take"]') && radios(document.querySelector('[data-q="take"]'),"take",
["A · Montreuil","B · Hamburg","both (Montreuil lead, Hamburg B-side)","undecided"]);
radios(document.querySelector('[data-q="loudness"]'),"loudness",
["−14 is right","want it louder","want more dynamic"]);
const onote=document.querySelector('[data-note="overall"]');
onote.value=store.note_overall||""; onote.oninput=()=>{store.note_overall=onote.value;save();};
// fragments
const fc=document.getElementById("frags");
FRAGS.forEach(f=>{ const c=document.createElement("div"); c.className="card";
const h=document.createElement("div"); h.className="q ttl"; h.textContent=f.q; c.appendChild(h);
const ab=document.createElement("div"); ab.className="ab";
[["A",f.a],["B",f.b]].forEach(([tag,o])=>{ const cc=document.createElement("div"); cc.className="card";
const row=document.createElement("div"); row.className="row"; const p=mkPlayer(o.file);
row.appendChild(p.btn);
const t=document.createElement("div"); t.innerHTML=`<span class="pill">${tag}</span> ${o.label}`;
row.appendChild(t); cc.appendChild(row); cc.appendChild(p.bw); ab.appendChild(cc); });
c.appendChild(ab);
const opts=document.createElement("div"); opts.className="q opts"; opts.textContent="Winner: ";
const span=document.createElement("span"); opts.appendChild(span);
radios(span, f.key, ["A","B","no preference"]); c.appendChild(opts);
const ta=document.createElement("textarea"); ta.rows=1; ta.placeholder="why?";
ta.value=store["note_"+f.key]||""; ta.oninput=()=>{store["note_"+f.key]=ta.value;save();};
c.appendChild(ta); fc.appendChild(c); });
// intro single
{ const c=document.createElement("div"); c.className="card";
const row=document.createElement("div"); row.className="row"; const p=mkPlayer(INTRO.file);
row.appendChild(p.btn);
const t=document.createElement("div"); t.innerHTML=`<div class="ttl">Intro check</div><div class="stats">${INTRO.q}</div>`;
row.appendChild(t); row.appendChild(p.bw); c.appendChild(row);
const ta=document.createElement("textarea"); ta.rows=1; ta.placeholder="intro notes…";
ta.value=store.note_intro||""; ta.oninput=()=>{store.note_intro=ta.value;save();};
c.appendChild(ta); fc.appendChild(c); }
function exportJSON(){
const blob=new Blob([JSON.stringify(store,null,2)],{type:"application/json"});
const a=document.createElement("a"); a.href=URL.createObjectURL(blob);
a.download="punkachien_verdicts.json"; a.click();
}
</script>
</body>
</html>
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
[
{
"title": "I come from Cyberspace",
"year": "2021",
"plays": 121,
"tier": "original",
"url": "https://soundcloud.com/parvagues/i-come-from-cyberspace",
"note": ""
},
{
"title": "Faith in Bass (feat. Sculpture)",
"year": "2022",
"plays": 53,
"tier": "original",
"url": "https://soundcloud.com/parvagues/faith-in-bass-feat-sculpture",
"note": ""
},
{
"title": "Soleil Pourpre",
"year": "2020",
"plays": 40,
"tier": "original",
"url": "https://soundcloud.com/parvagues/soleil-pourpre",
"note": ""
},
{
"title": "El Mundo Interior (toma uno)",
"year": "2024",
"plays": 31,
"tier": "original",
"url": "https://soundcloud.com/parvagues/el-mundo-interior",
"note": ""
},
{
"title": "Pixels Roses",
"year": "2021",
"plays": 30,
"tier": "original",
"url": "https://soundcloud.com/parvagues/pixels-roses",
"note": ""
},
{
"title": "Jardin d'Hiver",
"year": "2021",
"plays": 29,
"tier": "original",
"url": "https://soundcloud.com/parvagues/jardin-dhiver",
"note": ""
},
{
"title": "Tidal Crime Investigation",
"year": "2021",
"plays": 29,
"tier": "light",
"url": "https://soundcloud.com/parvagues/tidal-crime",
"note": ""
},
{
"title": "Au revoir Lord Toyota",
"year": "2020",
"plays": 29,
"tier": "original",
"url": "https://soundcloud.com/parvagues/au-revoir-lord-toyota",
"note": ""
},
{
"title": "Vie Hivernale",
"year": "2020",
"plays": 27,
"tier": "original",
"url": "https://soundcloud.com/parvagues/vie-hivernale",
"note": ""
},
{
"title": "Progressivement, Mardi",
"year": "2020",
"plays": 25,
"tier": "original",
"url": "https://soundcloud.com/parvagues/progressivement-mardi",
"note": ""
},
{
"title": "Maudite soit la Guerre",
"year": "2025",
"plays": 23,
"tier": "original",
"url": "https://soundcloud.com/parvagues/maudite-soit-la-guerre",
"note": ""
},
{
"title": "Stack Aérienne",
"year": "2020",
"plays": 22,
"tier": "original",
"url": "https://soundcloud.com/parvagues/stack-aerienne",
"note": ""
},
{
"title": "Premier Septembre (version longue)",
"year": "2025",
"plays": 21,
"tier": "original",
"url": "https://soundcloud.com/parvagues/premier-septembre",
"note": ""
},
{
"title": "Un dimanche mineur",
"year": "2021",
"plays": 21,
"tier": "original",
"url": "https://soundcloud.com/parvagues/un-dimanche-mineur",
"note": ""
},
{
"title": "Liquid Finale - feat @NVZT",
"year": "2025",
"plays": 19,
"tier": "original",
"url": "https://soundcloud.com/parvagues/liquid-finale",
"note": ""
},
{
"title": "Ré comme remède",
"year": "2020",
"plays": 17,
"tier": "original",
"url": "https://soundcloud.com/parvagues/re-comme-remede",
"note": ""
},
{
"title": "Calorifère",
"year": "2020",
"plays": 17,
"tier": "original",
"url": "https://soundcloud.com/parvagues/calorifere",
"note": ""
},
{
"title": "Du Code",
"year": "2020",
"plays": 17,
"tier": "original",
"url": "https://soundcloud.com/parvagues/du-code",
"note": ""
},
{
"title": "Transition à Fez : Ton numéro",
"year": "2025",
"plays": 16,
"tier": "light",
"url": "https://soundcloud.com/parvagues/transition-to-jam-de-fez-ton-numero-3",
"note": ""
},
{
"title": "Ton Numéro",
"year": "2025",
"plays": 16,
"tier": "light",
"url": "https://soundcloud.com/parvagues/ton-numero",
"note": ""
},
{
"title": "Deck the (Dance) Hall",
"year": "2024",
"plays": 15,
"tier": "original",
"url": "https://soundcloud.com/parvagues/deck-the-dance-hall",
"note": ""
},
{
"title": "Octobre Jaune",
"year": "2023",
"plays": 14,
"tier": "original",
"url": "https://soundcloud.com/parvagues/octobre-jaune",
"note": ""
},
{
"title": "Ciel Étoilé",
"year": "2020",
"plays": 14,
"tier": "original",
"url": "https://soundcloud.com/parvagues/ciel-etoile",
"note": ""
},
{
"title": "La Canopée",
"year": "2023",
"plays": 13,
"tier": "original",
"url": "https://soundcloud.com/parvagues/la-canopee",
"note": ""
},
{
"title": "Accel",
"year": "2020",
"plays": 13,
"tier": "original",
"url": "https://soundcloud.com/parvagues/accel",
"note": ""
},
{
"title": "Battements Printaniers",
"year": "2020",
"plays": 11,
"tier": "original",
"url": "https://soundcloud.com/parvagues/battements-printaniers",
"note": ""
},
{
"title": "It's About Time",
"year": "2023",
"plays": 10,
"tier": "original",
"url": "https://soundcloud.com/parvagues/its-about-time",
"note": ""
},
{
"title": "Chez Cricri - Épisode 1 : Intro Citare",
"year": "2025",
"plays": 9,
"tier": "original",
"url": "https://soundcloud.com/parvagues/chez-cricri1",
"note": ""
},
{
"title": "Smart Motivated Person",
"year": "2023",
"plays": 9,
"tier": "light",
"url": "https://soundcloud.com/parvagues/smart-motivated-person",
"note": ""
},
{
"title": "Dure Liberté",
"year": "2020",
"plays": 9,
"tier": "original",
"url": "https://soundcloud.com/parvagues/dure-liberte",
"note": ""
},
{
"title": "Prière Dure",
"year": "2020",
"plays": 8,
"tier": "original",
"url": "https://soundcloud.com/parvagues/priere-dure",
"note": ""
},
{
"title": "Bleu Cassé",
"year": "2020",
"plays": 7,
"tier": "original",
"url": "https://soundcloud.com/parvagues/bleucasse",
"note": ""
},
{
"title": "YouMay",
"year": "2020",
"plays": 7,
"tier": "original",
"url": "https://soundcloud.com/parvagues/youmay-feat-youc",
"note": ""
},
{
"title": "Take Five Drops",
"year": "2026",
"plays": 5,
"tier": "light",
"url": "https://soundcloud.com/parvagues/take-five-drops-2",
"note": ""
},
{
"title": "Piment Brésilien",
"year": "2026",
"plays": 4,
"tier": "original",
"url": "https://soundcloud.com/parvagues/piment-bresilien-1",
"note": ""
},
{
"title": "Pensif",
"year": "2020",
"plays": 4,
"tier": "original",
"url": "https://soundcloud.com/parvagues/pensif",
"note": ""
}
]
# Release Priority — recency-weighted set staples
Ranked by **how often + how recently** a track is played live, across 23 site setlists,
minus already-released (Apple∪Deezer, aka-aware). Weight = `0.6 ** (2026 - gig_year)`
(half-life ~1.35y), so a 2026 play counts ~4.6× a 2022 play. This is the canonical
input to `manifeste/calendar.md` — feed the top of this list into the monthly cadence.
| wscore | raw | last | track | years | source for master |
|--:|--:|--:|---|---|---|
| 4.92 | 9 | 2025 | **Sunny Side Up** | 24–25 | set stem / Prod |
| 3.46 | 8 | 2025 | **Salut Nu** | 23–25 | Audacity `SalutNu_r1` |
| 3.35 | 9 | 2025 | Contre Visite | 22–25 | older-rooted |
| 2.86 | 7 | 2025 | **Permanence** | 23–25 | set stem |
| 2.52 | 5 | 2025 | **Something about Drums** | 24–25 | Prod `SomethingAboutDnb` |
| 2.28 | 5 | 2025 | **Venons Ensemble** | 24–25 | CF album (not DSP) |
| 2.24 | 7 | 2025 | Invoque l'Été | 22–25 | older-rooted |
| 2.20 | 3 | 2026 | **PunkAChien** ↑ | 25–26 | Montreuil split ✓ |
| 2.16 | 4 | 2025 | **Bain Électrique** | 24–25 | CF album (not DSP) |
| 1.80 | 3 | 2025 | **Premier Septembre** (Sept1) | 25 | collab → split |
| 1.56 | 3 | 2025 | **Acidulé** | 24–25 | set stem |
| 1.60 | 2 | 2026 | Quand on Décolle · Piment Brésilien | 25–26 | Montreuil/CF split ✓ |
**Fresh 2026 Montreuil debuts (1× but current, masters READY in `tracks_bandcamp/`):**
Take 5 Drops · Super Sunny Side Up · W.I.P. W.A.P. · 'Plosive · Aria Sans Serif ·
You My Sunshine · Desire · Techno Orage · La Révolution Sera Samplée.
## Exclude (already released under FR/aka titles)
Blue Gold = L'Or Bleu · JeuDrill = Jeudi Drill · Fabuleux · Paroles d'Opal · Atlantis · Nass Arrive …
## Method note
Recurrence alone over-ranks faded Algolia-era staples (Contre Visite, Invoque l'Été);
recency weighting promotes what's *currently* in rotation (Sunny Side Up) and surfaces
brand-new 2026 material. Re-run when setlists update.
#!/usr/bin/env python3
"""Apply validated boundaries → re-split Take89 into per-track audio for listening.
Reads boundaries_take89_validated.json, sums the 12 Take89 orbit-stems per track
window (decode-accurate -ss seek, so only the window is read), loudnorm -14 for
comfortable audition, fades, FLAC + metadata tags (provenance-checked), and writes
a manifest. Output → freebox Prod/Montreuil26_resplit/ for PLN to listen.
NOTE: review-quality per-track audio (a normalized stem-sum), NOT final masters.
Final per-track mastering = tidal-ears master, later. Gig metadata pulled from the
canonical site lives content (see boundaries JSON `gig`), not invented.
"""
import json, subprocess, sys
from pathlib import Path
HERE = Path(__file__).parent
STEMS = Path("/mnt/freebox/PLN/Work/Sound/Prod/_stems/2026-05-22_Take89")
OUT = Path("/mnt/freebox/PLN/Work/Sound/Prod/Montreuil26_resplit")
B = json.loads((HERE / "boundaries_take89_validated.json").read_text())
# 15 tracks: track1 = opener, tracks 2..15 = each boundary's "into"
TRACK1 = "Quand on décolle"
bounds = B["boundaries"]
titles = [TRACK1] + [b["into"] for b in bounds]
starts = [0.0] + [b["t"] for b in bounds]
SET_END = 4690.0 # trailing; Revolution fades out (encore/claps trim = later edit)
ends = starts[1:] + [SET_END]
gig = B["gig"]
ALBUM = gig["set_title"]
def sanitize(s):
return "".join(c if c.isalnum() or c in " -_'" else "_" for c in s).strip()
def run(args):
r = subprocess.run(args, capture_output=True, text=True)
if r.returncode != 0:
print(r.stderr[-1500:], file=sys.stderr)
return r
def main():
OUT.mkdir(parents=True, exist_ok=True)
manifest = {"take": B["take"], "date": B["date"], "gig": gig,
"time_base": B["time_base"], "quality": "review (normalized stem-sum, -14 LUFS); not a final master",
"tracks": []}
for i, (title, s, e) in enumerate(zip(titles, starts, ends), 1):
dur = round(e - s, 2)
fn = f"{i:02d}-{sanitize(title)}.flac"
out = OUT / fn
# 12 orbit inputs, each seeked to s
inputs = []
for o in range(1, 13):
inputs += ["-accurate_seek", "-ss", str(s), "-t", str(dur),
"-i", str(STEMS / f"orbit-{o:02d}.flac")]
fo = max(0.0, dur - 3.0)
graph = (f"amix=inputs=12:normalize=0,volume=-9dB,"
f"afade=t=in:st=0:d=0.3,afade=t=out:st={fo}:d=3,"
f"loudnorm=I=-14:TP=-1:LRA=11")
tags = ["-metadata", f"title={title}", "-metadata", "artist=ParVagues",
"-metadata", f"album={ALBUM}", "-metadata", f"date={B['date']}",
"-metadata", f"track={i}/15",
"-metadata", f"comment=live @ {gig['venue']} | take {B['take']} | "
f"window {s}-{round(e,1)}s | src {gig['src']}"]
print(f" [{i:02d}/15] {title:<28} [{int(s//60)}:{int(s%60):02d}–{int(e//60)}:{int(e%60):02d}] {dur}s", flush=True)
r = run(["ffmpeg", "-hide_banner", "-loglevel", "error", *inputs,
"-filter_complex", graph, *tags,
"-ar", "44100", "-c:a", "flac", "-compression_level", "6",
str(out), "-y"])
ok = out.exists() and r.returncode == 0
manifest["tracks"].append({"n": i, "title": title, "file": fn,
"start_s": round(s, 1), "end_s": round(e, 1),
"dur_s": dur, "ok": ok})
(OUT / "manifest.json").write_text(json.dumps(manifest, indent=2))
print(f"\n✓ {sum(t['ok'] for t in manifest['tracks'])}/15 tracks → {OUT}")
print(f" manifest: {OUT}/manifest.json")
if __name__ == "__main__":
main()
This source diff could not be displayed because it is too large. You can view the blob instead.
#!/usr/bin/env python3
"""Seed the Take89 (Montreuil) master EDL from PLN's 2026-06-05 listening pass.
Turns the prose ear-feedback (performance_notes.md) into a typed, validated
MasterEDL — each decision replayable by a renderer AND usable as ground-truth for
the boundary/edge detectors (#25/#31). Re-run to regenerate; edit here, not the JSON.
"""
from datetime import date
from pathlib import Path
from models import (EditKind, EditOp, MasterEDL, MasterEdit, Provenance)
HERE = Path(__file__).parent
OUT = HERE / "master_edl_take89.json"
WH = "WH-1000XM5+phone"
def ear(loc):
return Provenance(source="ear", locator=loc, as_of=date(2026, 6, 5),
note="phone + WH-1000XM5 resplit review")
EDITS = [
MasterEdit(track_no=2, name="Piment Brésilien",
tidal="live/collab/raph/piment_bresilien.tidal",
op=EditOp.eq_highshelf, at="whole", value=-2.0, kind=EditKind.harsh,
device=WH, provenance=ear("aigus perçants sur mobile quand le lead/keys "
"montent + FX forts — tame ~4-8kHz, dynamic"),
reason="highs piercing on mobile when lead/keys climb high under heavy FX"),
MasterEdit(track_no=2, name="Piment Brésilien → Take 5 Drops",
op=EditOp.trim_out, at="-5", kind=EditKind.bad_cut, device=WH,
provenance=ear("on entend le changement de basse de la suivante AVANT le cut"),
reason="boundary ~5s too early: next track's bass change bleeds into tail"),
MasterEdit(track_no=3, name="Take 5 Drops → Super Sunny Side Up",
tidal="live/midi/nova/lounge/take_5_drops.tidal",
op=EditOp.crossfade, at="end", kind=EditKind.transition, device=WH,
provenance=ear("silence à la fin de la 3 puis 4 démarre direct — abrupt"),
reason="hard silence→onset butt-join; add short crossfade or trim-to-groove"),
MasterEdit(track_no=4, name="Super Sunny Side Up",
tidal="live/midi/nova/lounge/sunny_side_up.tidal",
op=EditOp.fade_out, at="4:00", to="4:09", kind=EditKind.laborious,
device=WH, provenance=ear("se terminerait à 4:00, ou 4:09 sur petit fade"),
reason="outro laborious — end at 4:00, or 4:09 with a small fade"),
MasterEdit(track_no=5, name="W.I.P. W.A.P.",
tidal="live/midi/nova/dnb/wap.tidal",
op=EditOp.trim_in, at="1.0", kind=EditKind.bad_cut, device=WH,
provenance=ear("commence sur 1s+ de break puis vocal — start au vocal"),
reason="trim ~1s break intro, start directly on the vocal"),
MasterEdit(track_no=5, name="W.I.P. W.A.P.",
tidal="live/midi/nova/dnb/wap.tidal",
op=EditOp.investigate, at="3:29", to="3:31", kind=EditKind.anomaly,
device=WH, provenance=ear("silence bizarre 3:29-3:31"),
reason="~2s odd silence/dropout — investigate (stem gap? mute glitch?)"),
MasterEdit(track_no=5, name="W.I.P. W.A.P. → 'Plosive",
tidal="live/midi/nova/dnb/wap.tidal",
op=EditOp.crossfade, at="3:26", to="3:45", kind=EditKind.transition,
device=WH, provenance=ear("bordel jusqu'à 3:42; crossfade direct 3:26→3:45+ ; "
"PLN veut tester des fragments"),
reason="messy tail 3:31-3:42; test fragment crossfades ~3:26→3:45 into 'Plosive"),
MasterEdit(track_no=6, name="'Plosive",
tidal="live/midi/nova/dnb/plosive.tidal",
op=EditOp.gain, at="kick", value=-2.0, kind=EditKind.level, device=WH,
provenance=ear("kick trop fort vs basses+mélodie; trop loud vs le kick des "
"autres pistes — set-relative match"),
reason="kick hotter than rest of set; match kick gain across tracks"),
MasterEdit(track_no=6, name="'Plosive → Jeudi Drill",
tidal="live/midi/nova/dnb/plosive.tidal",
op=EditOp.trim_out, at="2:59", kind=EditKind.bad_cut, device=WH,
provenance=ear("2:59 'Plosive ends, next sound is Bogdan Jeudrill sample"),
reason="runs into T7 Jeudi Drill (Bogdan voice) — cut the next-track head"),
]
def main():
edl = MasterEDL(take="Take89", gig="2026/montreuil-algorave", edits=EDITS)
OUT.write_text(edl.model_dump_json(indent=1))
n_label = sum(1 for e in EDITS if e.kind == EditKind.bad_cut)
print(f"✓ {OUT} ({len(EDITS)} edits, {n_label} bad_cut ground-truth labels)")
for e in edl.edits:
print(f" T{e.track_no or '?'} {e.name:<32} {e.op.value:<12} @{e.at:<6} "
f"[{e.kind.value}]")
if __name__ == "__main__":
main()
Take Five Drops (Demo)
Piment Bresilien (demo)
Algorave GZ25 - ParVagues & Pérégrine
Techno Oligarchie - Demo 2 -- Claude à la Basse
No Sunshine | #tidalcycles samples search UI prototype
ParVagues Viz - Editor Highlighting beta test
[Making of] Menthe Glacée: TidalCycles sampling of Assi El Hallani's Law Adri
Soleil d'Été - Première prise
cosmicfest v0 - extrait
En rentrant de l'AlgoRave
Al Salar Modular (#jamuary 03)
Septième de Noël 🎄🛤️ 🌄
la Bonne Pression - première prise (Jamuary 01)
Nuit étoilée pour Tom : Galaxie Opal 2024 🎇
ParVagues - Fabuleux ✨ (feat Amélie Oudéa Castera)
Live set for TopLap's Winter Solstice 2023 [Rehearsal]
Nuit Bleue - Jam TidalCycles + Modular Synth + Guitar
Session Orange
Réparation Nocturne | TidalCycles x Hydra.js Livecoding
Brouillard - TidalCycles Lo-fi impromptu livecoding
TidalCycles x LeapMotion : Contactless Music Livecoding 🪄
ParVagues | Nouveaux Horizons (feat. Édouard Philippe)
<!doctype html><meta charset=utf-8><title>PunkAChien — take compare</title>
<style>body{margin:0;font:15px/1.5 system-ui,sans-serif;color:#dff1f5;background:linear-gradient(#06121c,#02161f);min-height:100vh;padding:28px}
h1{color:#7fe3d4} .card{background:#0b2233;border:1px solid #1d4358;border-radius:14px;padding:18px;margin:14px 0;max-width:780px}
h2{margin:0 0 6px} .st{color:#8fb3c0;font-size:13px;margin-bottom:10px} .st b{color:#dff1f5}
audio{width:100%} .pth{color:#4a6b7a;font-size:11px;margin-top:6px;word-break:break-all}
.hint{color:#8fb3c0;font-size:13px;max-width:780px}</style>
<h1>⚖️ PunkAChien — compare takes</h1>
<p class=hint>Only one plays at a time. Pick the ship take, then I master it (→ 44.1 kHz / −14 LUFS).
If a player won't load (browser file:// block), the path is shown below it.</p>
<div class="card">
<h2>Montreuil 2026 — produced split</h2>
<div class="st"><b>-15.6</b> LUFS · peak <b>-1.0</b> dBFS · LRA <b>5.9</b> · 288s · 192 kHz</div>
<audio controls preload="none" src="file:///mnt/freebox/PLN/Work/Sound/Prod/Montreuil26_master/tracks_bandcamp/09-PunkAChien.flac"></audio>
<div class="pth">/mnt/freebox/PLN/Work/Sound/Prod/Montreuil26_master/tracks_bandcamp/09-PunkAChien.flac</div>
</div>
<div class="card">
<h2>Live @ Hamburg — 39C3, raw</h2>
<div class="st"><b>-18.5</b> LUFS · peak <b>-0.4</b> dBFS · LRA <b>5.9</b> · 142s · 44.1 kHz</div>
<audio controls preload="none" src="file:///mnt/freebox/PLN/Work/Sound/Prod/ParVagues%20-%20PunkaChien%20%28Live%40Hamburg%29.flac"></audio>
<div class="pth">/mnt/freebox/PLN/Work/Sound/Prod/ParVagues - PunkaChien (Live@Hamburg).flac</div>
</div>
<script>
const A=[...document.querySelectorAll('audio')];
A.forEach(a=>a.addEventListener('play',()=>A.forEach(o=>{if(o!==a)o.pause()})));
</script>
\ No newline at end of file
# Take → Gig map (dated, duration-corroborated)
_mtime≈gig date · duration: SET≥25m / track / sketch / empty(skip) · gig matched ±3d._
| date | take | dur | ch | class | gig (±days) |
|---|---|--:|--:|---|---|
| 2024-08-08 | Take3 | 7:07 | 12 | track | Opal Festival 2024 (±2d) |
| 2024-08-11 | Take4 | 57:22 | 12 | SET | Opal Festival 2024 (±1d) |
| 2024-08-16 | Take5 | 7:38 | 12 | track | |
| 2024-08-16 | Take8 | 7:12 | 12 | track | |
| 2024-08-27 | Take10 | 4:25 | 12 | track | |
| 2024-09-07 | Take14 | 7:00 | 12 | track | |
| 2024-09-10 | Take15 | 6:15 | 12 | track | |
| 2024-09-16 | Take16 | 8:10 | 12 | track | |
| 2024-09-16 | Take17 | 6:16 | 12 | track | |
| 2024-09-22 | Take18 | 5:03 | 13 | track | La French Stack (±2d) |
| 2024-09-22 | Take19 | 4:45 | 13 | track | La French Stack (±2d) |
| 2024-09-29 | Take20 | 71:39 | 13 | SET | CCC LIVE - Cookie Collective (±2d) |
| 2024-09-29 | Take21 | 16:09 | 13 | track | CCC LIVE - Cookie Collective (±2d) |
| 2024-10-05 | Take22 | 28:32 | 13 | SET | |
| 2024-10-19 | Take24 | 3:48 | 11 | track | |
| 2024-10-19 | Take28 | 0:00 | 8 | empty | |
| 2024-10-19 | Take29 | 7:32 | 13 | track | |
| 2024-11-16 | Take30 | 50:51 | 13 | SET | |
| 2024-11-24 | Take32 | 3:51 | 13 | track | |
| 2024-12-01 | Take33 | 2:02 | 13 | sketch | |
| 2024-12-20 | Take34 | 2:27 | 13 | sketch | TOPLAP Solstice 2024 (±1d) |
| 2024-12-25 | Take35 | 4:35 | 13 | track | [38C3] Secret Toilet Rave (±3d) |
| 2024-12-28 | Take36 | 61:46 | 12 | SET | [38C3] Secret Toilet Rave |
| 2024-12-29 | Take37 | 11:40 | 12 | track | [38C3] Chaos Music Club |
| 2024-12-29 | Take38 | 14:27 | 13 | track | [38C3] Chaos Music Club |
| 2025-01-15 | Take39 | 5:01 | 13 | track | |
| 2025-01-15 | Take40 | 4:45 | 13 | track | |
| 2025-01-18 | Take42 | 14:27 | 12 | track | |
| 2025-01-27 | Take45 | 4:01 | 11 | track | |
| 2025-01-27 | Take49 | 6:18 | 12 | track | |
| 2025-01-27 | Take53 | 5:37 | 12 | track | |
| 2025-02-04 | Take54 | 44:22 | 13 | SET | |
| 2025-02-06 | Take55 | 2:27 | 12 | sketch | |
| 2025-02-23 | Take58 | 16:26 | 12 | track | |
| 2025-03-20 | Take59 | 22:01 | 12 | track | |
| 2025-03-22 | Take60 | 47:08 | 12 | SET | |
| 2025-03-25 | Take61 | 46:02 | 12 | SET | |
| 2025-05-14 | Take62 | 4:31 | 11 | track | |
| 2025-05-19 | Take63 | 0:08 | 13 | empty | La French Stack (±1d) |
| 2025-05-19 | Take64 | 1:33 | 12 | sketch | La French Stack (±1d) |
| 2025-05-21 | Take65 | 36:29 | 12 | SET | LIVE CODING @ENSAD (±1d) |
| 2025-05-22 | Take66 | 82:20 | 12 | SET | LIVE CODING @ENSAD |
| 2025-05-24 | Take67 | 31:30 | 13 | SET | AlgoRave Lyon 2025 |
| 2025-05-25 | Take68 | 0:02 | 12 | empty | AlgoRave Lyon 2025 (±1d) |
| 2025-06-21 | Take70 | 100:19 | 12 | SET | CosmicFest v0 |
| 2025-06-22 | Take71 | 0:11 | 12 | empty | CosmicFest v0 (±1d) |
| 2025-06-23 | Take72 | 54:08 | 12 | SET | CosmicFest v0 (±2d) |
| 2025-07-08 | Take73 | 104:01 | 12 | SET | |
| 2025-08-10 | Take74 | 108:27 | 12 | SET | Opal Festival 2025 |
| 2025-08-13 | Take75 | 4:33 | 12 | track | Opal Festival 2025 (±3d) |
| 2025-09-20 | Take76 | 35:26 | 12 | SET | |
| 2025-09-25 | Take77 | 47:42 | 12 | SET | |
| 2025-10-19 | Take79 | 3:59 | 10 | track | |
| 2025-10-28 | Take80 | 2:57 | 8 | track | BUNKER (±3d) |
| 2025-11-08 | Take81 | 0:02 | 12 | empty | |
| 2025-11-08 | Take82 | 6:00 | 12 | track | |
| 2025-11-09 | Take83 | 31:00 | 12 | SET | |
| 2025-11-22 | Take84 | 3:59 | 12 | track | |
| 2025-11-23 | Take85 | 96:39 | 12 | SET | |
| 2025-11-30 | Take86 | 3:49 | 8 | track | |
| 2025-12-28 | Take87 | 55:59 | 12 | SET | [39C3] House of Tea |
| 2025-12-29 | Take88 | 92:32 | 12 | SET | [39C3] Toilet Rave |
| 2026-05-22 | Take89 | 78:18 | 12 | SET | Montreuil Algorave |
{
"schema": "locate-matrix L0 (metadata prior; nothing verified)",
"as_of": "2026-06-05",
"stats": {
"takes_total": 63,
"takes_matched_exact": 3,
"takes_matched_fuzzy": 14,
"takes_unmatched": 46,
"gigs_total": 23,
"gigs_with_take": 7,
"gigs_without_take": 16,
"distinct_tracks": 40,
"tracks_multi_take": 29,
"candidate_cells": 161
},
"release_readiness": [
{
"track": "live/midi/nova/lounge/sunny_side_up.tidal",
"aliases": [
"Sunny",
"Sunny Side Up",
"Super Sunny Side Up"
],
"n_takes": 11,
"takes": [
"Take18",
"Take19",
"Take63",
"Take64",
"Take65",
"Take66",
"Take70",
"Take71",
"Take72",
"Take80",
"Take89"
],
"gigs": [
"2024/la-french-stack",
"2025/bunker",
"2025/cosmicfest",
"2025/la-french-stack",
"2026/montreuil-algorave"
]
},
{
"track": "live/midi/nova/breaks/bain_electrique.tidal",
"aliases": [
"Bain Électrique",
"Bain électrique"
],
"n_takes": 9,
"takes": [
"Take18",
"Take19",
"Take63",
"Take64",
"Take65",
"Take66",
"Take70",
"Take71",
"Take72"
],
"gigs": [
"2024/la-french-stack",
"2025/cosmicfest",
"2025/la-french-stack"
]
},
{
"track": "live/midi/nova/dnb/force_motrice.tidal",
"aliases": [
"Atelier de Force Motrice",
"Force Motrice"
],
"n_takes": 9,
"takes": [
"Take18",
"Take19",
"Take20",
"Take21",
"Take63",
"Take64",
"Take65",
"Take66",
"Take80"
],
"gigs": [
"2024/ccc-live",
"2024/la-french-stack",
"2025/bunker",
"2025/la-french-stack"
]
},
{
"track": "live/midi/nova/lounge/suns_of_gold.tidal",
"aliases": [
"L'Or Bleu"
],
"n_takes": 9,
"takes": [
"Take18",
"Take19",
"Take63",
"Take64",
"Take65",
"Take66",
"Take70",
"Take71",
"Take72"
],
"gigs": [
"2024/la-french-stack",
"2025/cosmicfest",
"2025/la-french-stack"
]
},
{
"track": "live/midi/nova/nujazz/salut_nu.tidal",
"aliases": [
"Salut Nu"
],
"n_takes": 8,
"takes": [
"Take18",
"Take19",
"Take20",
"Take21",
"Take63",
"Take64",
"Take65",
"Take66"
],
"gigs": [
"2024/ccc-live",
"2024/la-french-stack",
"2025/la-french-stack"
]
},
{
"track": "live/midi/nova/nujazz/cafe_tiede.tidal",
"aliases": [
"Cafe Tiede",
"Café Tiède"
],
"n_takes": 8,
"takes": [
"Take18",
"Take19",
"Take20",
"Take21",
"Take63",
"Take64",
"Take65",
"Take66"
],
"gigs": [
"2024/ccc-live",
"2024/la-french-stack",
"2025/la-french-stack"
]
},
{
"track": "live/collab/raph/acidule.tidal",
"aliases": [
"Acidule",
"Acidulé",
"L'ACID d'abord <3"
],
"n_takes": 7,
"takes": [
"Take20",
"Take21",
"Take35",
"Take36",
"Take37",
"Take38",
"Take80"
],
"gigs": [
"2024/38c3-toilet",
"2024/ccc-live",
"2025/bunker"
]
},
{
"track": "live/collab/ccc/ghosts_in_the_toilets.tidal",
"aliases": [
"Ghosts in the T01l3ts",
"Ghosts in the Toilets"
],
"n_takes": 7,
"takes": [
"Take35",
"Take36",
"Take37",
"Take38",
"Take70",
"Take71",
"Take72"
],
"gigs": [
"2024/38c3-toilet",
"2025/cosmicfest"
]
},
{
"track": "live/midi/nova/dnb/venons_ensemble.tidal",
"aliases": [
"Venons Ensemble"
],
"n_takes": 7,
"takes": [
"Take63",
"Take64",
"Take65",
"Take66",
"Take70",
"Take71",
"Take72"
],
"gigs": [
"2025/cosmicfest",
"2025/la-french-stack"
]
},
{
"track": "live/midi/nova/ambient/contre_visite.tidal",
"aliases": [
"Contre Visite"
],
"n_takes": 6,
"takes": [
"Take18",
"Take19",
"Take63",
"Take64",
"Take65",
"Take66"
],
"gigs": [
"2024/la-french-stack",
"2025/la-french-stack"
]
},
{
"track": "live/midi/nova/dnb/something_about_drums.tidal",
"aliases": [
"Something about Drums"
],
"n_takes": 6,
"takes": [
"Take18",
"Take19",
"Take63",
"Take64",
"Take65",
"Take66"
],
"gigs": [
"2024/la-french-stack",
"2025/la-french-stack"
]
},
{
"track": "live/collab/raph/permanence.tidal",
"aliases": [
"Permanence"
],
"n_takes": 6,
"takes": [
"Take18",
"Take19",
"Take63",
"Take64",
"Take65",
"Take66"
],
"gigs": [
"2024/la-french-stack",
"2025/la-french-stack"
]
},
{
"track": "live/collab/raph/punkachien.tidal",
"aliases": [
"Pitbul Punk",
"PunkAChien"
],
"n_takes": 5,
"takes": [
"Take35",
"Take36",
"Take37",
"Take38",
"Take89"
],
"gigs": [
"2024/38c3-toilet",
"2026/montreuil-algorave"
]
},
{
"track": "live/collab/mousquetaires/blue_gold.tidal",
"aliases": [
"L'or Bleu"
],
"n_takes": 5,
"takes": [
"Take35",
"Take36",
"Take37",
"Take38",
"Take89"
],
"gigs": [
"2024/38c3-toilet",
"2026/montreuil-algorave"
]
},
{
"track": "live/midi/nova/techno/techno_orage.tidal",
"aliases": [
"Nuit d'orage",
"Orage",
"Techno Orage"
],
"n_takes": 5,
"takes": [
"Take70",
"Take71",
"Take72",
"Take80",
"Take89"
],
"gigs": [
"2025/bunker",
"2025/cosmicfest",
"2026/montreuil-algorave"
]
},
{
"track": "live/collab/baba/sept1.tidal",
"aliases": [
"Premier Septembre",
"Sept1"
],
"n_takes": 5,
"takes": [
"Take70",
"Take71",
"Take72",
"Take80",
"Take89"
],
"gigs": [
"2025/bunker",
"2025/cosmicfest",
"2026/montreuil-algorave"
]
},
{
"track": "live/collab/raph/nouveau_punk.tidal",
"aliases": [
"Nouveau Punk"
],
"n_takes": 4,
"takes": [
"Take35",
"Take36",
"Take37",
"Take38"
],
"gigs": [
"2024/38c3-toilet"
]
},
{
"track": "live/midi/nova/ambient/quand_on_decolle.tidal",
"aliases": [
"Quand on Décolle",
"Quand on décolle"
],
"n_takes": 4,
"takes": [
"Take70",
"Take71",
"Take72",
"Take89"
],
"gigs": [
"2025/cosmicfest",
"2026/montreuil-algorave"
]
},
{
"track": "live/collab/raph/jeudrill.tidal",
"aliases": [
"Jeudi Drill"
],
"n_takes": 4,
"takes": [
"Take70",
"Take71",
"Take72",
"Take89"
],
"gigs": [
"2025/cosmicfest",
"2026/montreuil-algorave"
]
},
{
"track": "live/midi/nova/lounge/fabuleux.tidal",
"aliases": [
"Fabuleux"
],
"n_takes": 3,
"takes": [
"Take70",
"Take71",
"Take72"
],
"gigs": [
"2025/cosmicfest"
]
},
{
"track": "live/midi/nova/breaks/madeleine_de_paris.tidal",
"aliases": [
"Paris"
],
"n_takes": 3,
"takes": [
"Take70",
"Take71",
"Take72"
],
"gigs": [
"2025/cosmicfest"
]
},
{
"track": "live/midi/nova/beatober/oct_16_haunted_house_insouciance.tidal",
"aliases": [
"La fin de l'insouciance"
],
"n_takes": 3,
"takes": [
"Take70",
"Take71",
"Take72"
],
"gigs": [
"2025/cosmicfest"
]
},
{
"track": "live/midi/nova/breaks/lady_perplexity.tidal",
"aliases": [
"Lady Perplexity"
],
"n_takes": 3,
"takes": [
"Take70",
"Take71",
"Take72"
],
"gigs": [
"2025/cosmicfest"
]
},
{
"track": "live/midi/nova/techno/ete_a_mauerpark.tidal",
"aliases": [
"L'été à Mauerpark"
],
"n_takes": 3,
"takes": [
"Take70",
"Take71",
"Take72"
],
"gigs": [
"2025/cosmicfest"
]
},
{
"track": "live/midi/nova/dnb/alerte_verte.tidal",
"aliases": [
"Alerte Verte"
],
"n_takes": 2,
"takes": [
"Take20",
"Take21"
],
"gigs": [
"2024/ccc-live"
]
},
{
"track": "live/collab/ccc/ccc0.tidal",
"aliases": [
"Blue Gold"
],
"n_takes": 2,
"takes": [
"Take20",
"Take21"
],
"gigs": [
"2024/ccc-live"
]
},
{
"track": "live/midi/nova/breaks/nuit_agitee.tidal",
"aliases": [
"Nuit Agitee"
],
"n_takes": 2,
"takes": [
"Take20",
"Take21"
],
"gigs": [
"2024/ccc-live"
]
},
{
"track": "live/dnb/nass_revient.tidal",
"aliases": [
"Nass Revient de Mars!"
],
"n_takes": 2,
"takes": [
"Take20",
"Take21"
],
"gigs": [
"2024/ccc-live"
]
},
{
"track": "live/midi/nova/nujazz/cafe_glace.tidal",
"aliases": [
"Cafe Glace"
],
"n_takes": 2,
"takes": [
"Take20",
"Take21"
],
"gigs": [
"2024/ccc-live"
]
},
{
"track": "live/collab/jane/drifting_soul.tidal",
"aliases": [
"Drifting Soul"
],
"n_takes": 1,
"takes": [
"Take80"
],
"gigs": [
"2025/bunker"
]
},
{
"track": "copycat/because_its_there.tidal",
"aliases": [
"Because It's There"
],
"n_takes": 1,
"takes": [
"Take80"
],
"gigs": [
"2025/bunker"
]
},
{
"track": "live/midi/nova/techno/ere_de_jeu.tidal",
"aliases": [
"Ere de Jeu"
],
"n_takes": 1,
"takes": [
"Take80"
],
"gigs": [
"2025/bunker"
]
},
{
"track": "live/collab/raph/piment_bresilien.tidal",
"aliases": [
"Piment brésilien"
],
"n_takes": 1,
"takes": [
"Take89"
],
"gigs": [
"2026/montreuil-algorave"
]
},
{
"track": "live/midi/nova/lounge/take_5_drops.tidal",
"aliases": [
"Take 5 Drops"
],
"n_takes": 1,
"takes": [
"Take89"
],
"gigs": [
"2026/montreuil-algorave"
]
},
{
"track": "live/midi/nova/dnb/wap.tidal",
"aliases": [
"W.I.P. W.A.P."
],
"n_takes": 1,
"takes": [
"Take89"
],
"gigs": [
"2026/montreuil-algorave"
]
},
{
"track": "live/midi/nova/dnb/plosive.tidal",
"aliases": [
"'Plosive"
],
"n_takes": 1,
"takes": [
"Take89"
],
"gigs": [
"2026/montreuil-algorave"
]
},
{
"track": "live/collab/raph/aria_sans_serif.tidal",
"aliases": [
"Aria Sans Serif"
],
"n_takes": 1,
"takes": [
"Take89"
],
"gigs": [
"2026/montreuil-algorave"
]
},
{
"track": "live/midi/nova/dnb/liquid/you_my_sunshine.tidal",
"aliases": [
"You My Sunshine"
],
"n_takes": 1,
"takes": [
"Take89"
],
"gigs": [
"2026/montreuil-algorave"
]
},
{
"track": "live/collab/raph/desire.tidal",
"aliases": [
"Desire"
],
"n_takes": 1,
"takes": [
"Take89"
],
"gigs": [
"2026/montreuil-algorave"
]
},
{
"track": "live/midi/nova/jazz/the_revolution_will_be_sampled.tidal",
"aliases": [
"La Révolution Sera Samplée"
],
"n_takes": 1,
"takes": [
"Take89"
],
"gigs": [
"2026/montreuil-algorave"
]
}
],
"cells": [
{
"track": "live/midi/nova/ambient/contre_visite.tidal",
"name": "Contre Visite",
"tidal": "live/midi/nova/ambient/contre_visite.tidal",
"take": "Take18",
"gig": "2024/la-french-stack",
"take_type": "track",
"bpm": 90,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/breaks/bain_electrique.tidal",
"name": "Bain Électrique",
"tidal": "live/midi/nova/breaks/bain_electrique.tidal",
"take": "Take18",
"gig": "2024/la-french-stack",
"take_type": "track",
"bpm": 128,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/nujazz/salut_nu.tidal",
"name": "Salut Nu",
"tidal": "live/midi/nova/nujazz/salut_nu.tidal",
"take": "Take18",
"gig": "2024/la-french-stack",
"take_type": "track",
"bpm": 120,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/nujazz/cafe_tiede.tidal",
"name": "Café Tiède",
"tidal": "live/midi/nova/nujazz/cafe_tiede.tidal",
"take": "Take18",
"gig": "2024/la-french-stack",
"take_type": "track",
"bpm": 125,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/force_motrice.tidal",
"name": "Force Motrice",
"tidal": "live/midi/nova/dnb/force_motrice.tidal",
"take": "Take18",
"gig": "2024/la-french-stack",
"take_type": "track",
"bpm": 125,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/lounge/sunny_side_up.tidal",
"name": "Sunny Side Up",
"tidal": "live/midi/nova/lounge/sunny_side_up.tidal",
"take": "Take18",
"gig": "2024/la-french-stack",
"take_type": "track",
"bpm": 120,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/something_about_drums.tidal",
"name": "Something about Drums",
"tidal": "live/midi/nova/dnb/something_about_drums.tidal",
"take": "Take18",
"gig": "2024/la-french-stack",
"take_type": "track",
"bpm": 160,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/permanence.tidal",
"name": "Permanence",
"tidal": "live/collab/raph/permanence.tidal",
"take": "Take18",
"gig": "2024/la-french-stack",
"take_type": "track",
"bpm": 150,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/lounge/suns_of_gold.tidal",
"name": "L'Or Bleu",
"tidal": "live/midi/nova/lounge/suns_of_gold.tidal",
"take": "Take18",
"gig": "2024/la-french-stack",
"take_type": "track",
"bpm": 94,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/ambient/contre_visite.tidal",
"name": "Contre Visite",
"tidal": "live/midi/nova/ambient/contre_visite.tidal",
"take": "Take19",
"gig": "2024/la-french-stack",
"take_type": "track",
"bpm": 90,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/breaks/bain_electrique.tidal",
"name": "Bain Électrique",
"tidal": "live/midi/nova/breaks/bain_electrique.tidal",
"take": "Take19",
"gig": "2024/la-french-stack",
"take_type": "track",
"bpm": 128,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/nujazz/salut_nu.tidal",
"name": "Salut Nu",
"tidal": "live/midi/nova/nujazz/salut_nu.tidal",
"take": "Take19",
"gig": "2024/la-french-stack",
"take_type": "track",
"bpm": 120,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/nujazz/cafe_tiede.tidal",
"name": "Café Tiède",
"tidal": "live/midi/nova/nujazz/cafe_tiede.tidal",
"take": "Take19",
"gig": "2024/la-french-stack",
"take_type": "track",
"bpm": 125,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/force_motrice.tidal",
"name": "Force Motrice",
"tidal": "live/midi/nova/dnb/force_motrice.tidal",
"take": "Take19",
"gig": "2024/la-french-stack",
"take_type": "track",
"bpm": 125,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/lounge/sunny_side_up.tidal",
"name": "Sunny Side Up",
"tidal": "live/midi/nova/lounge/sunny_side_up.tidal",
"take": "Take19",
"gig": "2024/la-french-stack",
"take_type": "track",
"bpm": 120,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/something_about_drums.tidal",
"name": "Something about Drums",
"tidal": "live/midi/nova/dnb/something_about_drums.tidal",
"take": "Take19",
"gig": "2024/la-french-stack",
"take_type": "track",
"bpm": 160,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/permanence.tidal",
"name": "Permanence",
"tidal": "live/collab/raph/permanence.tidal",
"take": "Take19",
"gig": "2024/la-french-stack",
"take_type": "track",
"bpm": 150,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/lounge/suns_of_gold.tidal",
"name": "L'Or Bleu",
"tidal": "live/midi/nova/lounge/suns_of_gold.tidal",
"take": "Take19",
"gig": "2024/la-french-stack",
"take_type": "track",
"bpm": 94,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/acidule.tidal",
"name": "L'ACID d'abord <3",
"tidal": "live/collab/raph/acidule.tidal",
"take": "Take20",
"gig": "2024/ccc-live",
"take_type": "SET",
"bpm": 135,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/alerte_verte.tidal",
"name": "Alerte Verte",
"tidal": "live/midi/nova/dnb/alerte_verte.tidal",
"take": "Take20",
"gig": "2024/ccc-live",
"take_type": "SET",
"bpm": 160,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/ccc/ccc0.tidal",
"name": "Blue Gold",
"tidal": "live/collab/ccc/ccc0.tidal",
"take": "Take20",
"gig": "2024/ccc-live",
"take_type": "SET",
"bpm": 140,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/breaks/nuit_agitee.tidal",
"name": "Nuit Agitee",
"tidal": "live/midi/nova/breaks/nuit_agitee.tidal",
"take": "Take20",
"gig": "2024/ccc-live",
"take_type": "SET",
"bpm": 160,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/dnb/nass_revient.tidal",
"name": "Nass Revient de Mars!",
"tidal": "live/dnb/nass_revient.tidal",
"take": "Take20",
"gig": "2024/ccc-live",
"take_type": "SET",
"bpm": 140,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/force_motrice.tidal",
"name": "Atelier de Force Motrice",
"tidal": "live/midi/nova/dnb/force_motrice.tidal",
"take": "Take20",
"gig": "2024/ccc-live",
"take_type": "SET",
"bpm": 125,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/nujazz/cafe_tiede.tidal",
"name": "Cafe Tiede",
"tidal": "live/midi/nova/nujazz/cafe_tiede.tidal",
"take": "Take20",
"gig": "2024/ccc-live",
"take_type": "SET",
"bpm": 125,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/nujazz/cafe_glace.tidal",
"name": "Cafe Glace",
"tidal": "live/midi/nova/nujazz/cafe_glace.tidal",
"take": "Take20",
"gig": "2024/ccc-live",
"take_type": "SET",
"bpm": 120,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/nujazz/salut_nu.tidal",
"name": "Salut Nu",
"tidal": "live/midi/nova/nujazz/salut_nu.tidal",
"take": "Take20",
"gig": "2024/ccc-live",
"take_type": "SET",
"bpm": 120,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/acidule.tidal",
"name": "L'ACID d'abord <3",
"tidal": "live/collab/raph/acidule.tidal",
"take": "Take21",
"gig": "2024/ccc-live",
"take_type": "track",
"bpm": 135,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/alerte_verte.tidal",
"name": "Alerte Verte",
"tidal": "live/midi/nova/dnb/alerte_verte.tidal",
"take": "Take21",
"gig": "2024/ccc-live",
"take_type": "track",
"bpm": 160,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/ccc/ccc0.tidal",
"name": "Blue Gold",
"tidal": "live/collab/ccc/ccc0.tidal",
"take": "Take21",
"gig": "2024/ccc-live",
"take_type": "track",
"bpm": 140,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/breaks/nuit_agitee.tidal",
"name": "Nuit Agitee",
"tidal": "live/midi/nova/breaks/nuit_agitee.tidal",
"take": "Take21",
"gig": "2024/ccc-live",
"take_type": "track",
"bpm": 160,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/dnb/nass_revient.tidal",
"name": "Nass Revient de Mars!",
"tidal": "live/dnb/nass_revient.tidal",
"take": "Take21",
"gig": "2024/ccc-live",
"take_type": "track",
"bpm": 140,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/force_motrice.tidal",
"name": "Atelier de Force Motrice",
"tidal": "live/midi/nova/dnb/force_motrice.tidal",
"take": "Take21",
"gig": "2024/ccc-live",
"take_type": "track",
"bpm": 125,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/nujazz/cafe_tiede.tidal",
"name": "Cafe Tiede",
"tidal": "live/midi/nova/nujazz/cafe_tiede.tidal",
"take": "Take21",
"gig": "2024/ccc-live",
"take_type": "track",
"bpm": 125,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/nujazz/cafe_glace.tidal",
"name": "Cafe Glace",
"tidal": "live/midi/nova/nujazz/cafe_glace.tidal",
"take": "Take21",
"gig": "2024/ccc-live",
"take_type": "track",
"bpm": 120,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/nujazz/salut_nu.tidal",
"name": "Salut Nu",
"tidal": "live/midi/nova/nujazz/salut_nu.tidal",
"take": "Take21",
"gig": "2024/ccc-live",
"take_type": "track",
"bpm": 120,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/ccc/ghosts_in_the_toilets.tidal",
"name": "Ghosts in the Toilets",
"tidal": "live/collab/ccc/ghosts_in_the_toilets.tidal",
"take": "Take35",
"gig": "2024/38c3-toilet",
"take_type": "track",
"bpm": 160,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±3d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/nouveau_punk.tidal",
"name": "Nouveau Punk",
"tidal": "live/collab/raph/nouveau_punk.tidal",
"take": "Take35",
"gig": "2024/38c3-toilet",
"take_type": "track",
"bpm": 155,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±3d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/punkachien.tidal",
"name": "Pitbul Punk",
"tidal": "live/collab/raph/punkachien.tidal",
"take": "Take35",
"gig": "2024/38c3-toilet",
"take_type": "track",
"bpm": 170,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±3d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/acidule.tidal",
"name": "Acidulé",
"tidal": "live/collab/raph/acidule.tidal",
"take": "Take35",
"gig": "2024/38c3-toilet",
"take_type": "track",
"bpm": 135,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±3d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/mousquetaires/blue_gold.tidal",
"name": "L'or Bleu",
"tidal": "live/collab/mousquetaires/blue_gold.tidal",
"take": "Take35",
"gig": "2024/38c3-toilet",
"take_type": "track",
"bpm": 124,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±3d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/ccc/ghosts_in_the_toilets.tidal",
"name": "Ghosts in the Toilets",
"tidal": "live/collab/ccc/ghosts_in_the_toilets.tidal",
"take": "Take36",
"gig": "2024/38c3-toilet",
"take_type": "SET",
"bpm": 160,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/nouveau_punk.tidal",
"name": "Nouveau Punk",
"tidal": "live/collab/raph/nouveau_punk.tidal",
"take": "Take36",
"gig": "2024/38c3-toilet",
"take_type": "SET",
"bpm": 155,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/punkachien.tidal",
"name": "Pitbul Punk",
"tidal": "live/collab/raph/punkachien.tidal",
"take": "Take36",
"gig": "2024/38c3-toilet",
"take_type": "SET",
"bpm": 170,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/acidule.tidal",
"name": "Acidulé",
"tidal": "live/collab/raph/acidule.tidal",
"take": "Take36",
"gig": "2024/38c3-toilet",
"take_type": "SET",
"bpm": 135,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/mousquetaires/blue_gold.tidal",
"name": "L'or Bleu",
"tidal": "live/collab/mousquetaires/blue_gold.tidal",
"take": "Take36",
"gig": "2024/38c3-toilet",
"take_type": "SET",
"bpm": 124,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/ccc/ghosts_in_the_toilets.tidal",
"name": "Ghosts in the Toilets",
"tidal": "live/collab/ccc/ghosts_in_the_toilets.tidal",
"take": "Take37",
"gig": "2024/38c3-toilet",
"take_type": "track",
"bpm": 160,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/nouveau_punk.tidal",
"name": "Nouveau Punk",
"tidal": "live/collab/raph/nouveau_punk.tidal",
"take": "Take37",
"gig": "2024/38c3-toilet",
"take_type": "track",
"bpm": 155,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/punkachien.tidal",
"name": "Pitbul Punk",
"tidal": "live/collab/raph/punkachien.tidal",
"take": "Take37",
"gig": "2024/38c3-toilet",
"take_type": "track",
"bpm": 170,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/acidule.tidal",
"name": "Acidulé",
"tidal": "live/collab/raph/acidule.tidal",
"take": "Take37",
"gig": "2024/38c3-toilet",
"take_type": "track",
"bpm": 135,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/mousquetaires/blue_gold.tidal",
"name": "L'or Bleu",
"tidal": "live/collab/mousquetaires/blue_gold.tidal",
"take": "Take37",
"gig": "2024/38c3-toilet",
"take_type": "track",
"bpm": 124,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/ccc/ghosts_in_the_toilets.tidal",
"name": "Ghosts in the Toilets",
"tidal": "live/collab/ccc/ghosts_in_the_toilets.tidal",
"take": "Take38",
"gig": "2024/38c3-toilet",
"take_type": "track",
"bpm": 160,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/nouveau_punk.tidal",
"name": "Nouveau Punk",
"tidal": "live/collab/raph/nouveau_punk.tidal",
"take": "Take38",
"gig": "2024/38c3-toilet",
"take_type": "track",
"bpm": 155,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/punkachien.tidal",
"name": "Pitbul Punk",
"tidal": "live/collab/raph/punkachien.tidal",
"take": "Take38",
"gig": "2024/38c3-toilet",
"take_type": "track",
"bpm": 170,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/acidule.tidal",
"name": "Acidulé",
"tidal": "live/collab/raph/acidule.tidal",
"take": "Take38",
"gig": "2024/38c3-toilet",
"take_type": "track",
"bpm": 135,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/mousquetaires/blue_gold.tidal",
"name": "L'or Bleu",
"tidal": "live/collab/mousquetaires/blue_gold.tidal",
"take": "Take38",
"gig": "2024/38c3-toilet",
"take_type": "track",
"bpm": 124,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/ambient/contre_visite.tidal",
"name": "Contre Visite",
"tidal": "live/midi/nova/ambient/contre_visite.tidal",
"take": "Take63",
"gig": "2025/la-french-stack",
"take_type": "empty",
"bpm": 90,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/breaks/bain_electrique.tidal",
"name": "Bain Électrique",
"tidal": "live/midi/nova/breaks/bain_electrique.tidal",
"take": "Take63",
"gig": "2025/la-french-stack",
"take_type": "empty",
"bpm": 128,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/nujazz/salut_nu.tidal",
"name": "Salut Nu",
"tidal": "live/midi/nova/nujazz/salut_nu.tidal",
"take": "Take63",
"gig": "2025/la-french-stack",
"take_type": "empty",
"bpm": 120,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/nujazz/cafe_tiede.tidal",
"name": "Café Tiède",
"tidal": "live/midi/nova/nujazz/cafe_tiede.tidal",
"take": "Take63",
"gig": "2025/la-french-stack",
"take_type": "empty",
"bpm": 125,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/force_motrice.tidal",
"name": "Force Motrice",
"tidal": "live/midi/nova/dnb/force_motrice.tidal",
"take": "Take63",
"gig": "2025/la-french-stack",
"take_type": "empty",
"bpm": 125,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/lounge/sunny_side_up.tidal",
"name": "Sunny Side Up",
"tidal": "live/midi/nova/lounge/sunny_side_up.tidal",
"take": "Take63",
"gig": "2025/la-french-stack",
"take_type": "empty",
"bpm": 120,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/venons_ensemble.tidal",
"name": "Venons Ensemble",
"tidal": "live/midi/nova/dnb/venons_ensemble.tidal",
"take": "Take63",
"gig": "2025/la-french-stack",
"take_type": "empty",
"bpm": 85,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/something_about_drums.tidal",
"name": "Something about Drums",
"tidal": "live/midi/nova/dnb/something_about_drums.tidal",
"take": "Take63",
"gig": "2025/la-french-stack",
"take_type": "empty",
"bpm": 160,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/permanence.tidal",
"name": "Permanence",
"tidal": "live/collab/raph/permanence.tidal",
"take": "Take63",
"gig": "2025/la-french-stack",
"take_type": "empty",
"bpm": 150,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/lounge/suns_of_gold.tidal",
"name": "L'Or Bleu",
"tidal": "live/midi/nova/lounge/suns_of_gold.tidal",
"take": "Take63",
"gig": "2025/la-french-stack",
"take_type": "empty",
"bpm": 94,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/ambient/contre_visite.tidal",
"name": "Contre Visite",
"tidal": "live/midi/nova/ambient/contre_visite.tidal",
"take": "Take64",
"gig": "2025/la-french-stack",
"take_type": "sketch",
"bpm": 90,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/breaks/bain_electrique.tidal",
"name": "Bain Électrique",
"tidal": "live/midi/nova/breaks/bain_electrique.tidal",
"take": "Take64",
"gig": "2025/la-french-stack",
"take_type": "sketch",
"bpm": 128,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/nujazz/salut_nu.tidal",
"name": "Salut Nu",
"tidal": "live/midi/nova/nujazz/salut_nu.tidal",
"take": "Take64",
"gig": "2025/la-french-stack",
"take_type": "sketch",
"bpm": 120,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/nujazz/cafe_tiede.tidal",
"name": "Café Tiède",
"tidal": "live/midi/nova/nujazz/cafe_tiede.tidal",
"take": "Take64",
"gig": "2025/la-french-stack",
"take_type": "sketch",
"bpm": 125,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/force_motrice.tidal",
"name": "Force Motrice",
"tidal": "live/midi/nova/dnb/force_motrice.tidal",
"take": "Take64",
"gig": "2025/la-french-stack",
"take_type": "sketch",
"bpm": 125,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/lounge/sunny_side_up.tidal",
"name": "Sunny Side Up",
"tidal": "live/midi/nova/lounge/sunny_side_up.tidal",
"take": "Take64",
"gig": "2025/la-french-stack",
"take_type": "sketch",
"bpm": 120,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/venons_ensemble.tidal",
"name": "Venons Ensemble",
"tidal": "live/midi/nova/dnb/venons_ensemble.tidal",
"take": "Take64",
"gig": "2025/la-french-stack",
"take_type": "sketch",
"bpm": 85,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/something_about_drums.tidal",
"name": "Something about Drums",
"tidal": "live/midi/nova/dnb/something_about_drums.tidal",
"take": "Take64",
"gig": "2025/la-french-stack",
"take_type": "sketch",
"bpm": 160,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/permanence.tidal",
"name": "Permanence",
"tidal": "live/collab/raph/permanence.tidal",
"take": "Take64",
"gig": "2025/la-french-stack",
"take_type": "sketch",
"bpm": 150,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/lounge/suns_of_gold.tidal",
"name": "L'Or Bleu",
"tidal": "live/midi/nova/lounge/suns_of_gold.tidal",
"take": "Take64",
"gig": "2025/la-french-stack",
"take_type": "sketch",
"bpm": 94,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/ambient/contre_visite.tidal",
"name": "Contre Visite",
"tidal": "live/midi/nova/ambient/contre_visite.tidal",
"take": "Take65",
"gig": "2025/la-french-stack",
"take_type": "SET",
"bpm": 90,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/breaks/bain_electrique.tidal",
"name": "Bain Électrique",
"tidal": "live/midi/nova/breaks/bain_electrique.tidal",
"take": "Take65",
"gig": "2025/la-french-stack",
"take_type": "SET",
"bpm": 128,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/nujazz/salut_nu.tidal",
"name": "Salut Nu",
"tidal": "live/midi/nova/nujazz/salut_nu.tidal",
"take": "Take65",
"gig": "2025/la-french-stack",
"take_type": "SET",
"bpm": 120,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/nujazz/cafe_tiede.tidal",
"name": "Café Tiède",
"tidal": "live/midi/nova/nujazz/cafe_tiede.tidal",
"take": "Take65",
"gig": "2025/la-french-stack",
"take_type": "SET",
"bpm": 125,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/force_motrice.tidal",
"name": "Force Motrice",
"tidal": "live/midi/nova/dnb/force_motrice.tidal",
"take": "Take65",
"gig": "2025/la-french-stack",
"take_type": "SET",
"bpm": 125,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/lounge/sunny_side_up.tidal",
"name": "Sunny Side Up",
"tidal": "live/midi/nova/lounge/sunny_side_up.tidal",
"take": "Take65",
"gig": "2025/la-french-stack",
"take_type": "SET",
"bpm": 120,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/venons_ensemble.tidal",
"name": "Venons Ensemble",
"tidal": "live/midi/nova/dnb/venons_ensemble.tidal",
"take": "Take65",
"gig": "2025/la-french-stack",
"take_type": "SET",
"bpm": 85,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/something_about_drums.tidal",
"name": "Something about Drums",
"tidal": "live/midi/nova/dnb/something_about_drums.tidal",
"take": "Take65",
"gig": "2025/la-french-stack",
"take_type": "SET",
"bpm": 160,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/permanence.tidal",
"name": "Permanence",
"tidal": "live/collab/raph/permanence.tidal",
"take": "Take65",
"gig": "2025/la-french-stack",
"take_type": "SET",
"bpm": 150,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/lounge/suns_of_gold.tidal",
"name": "L'Or Bleu",
"tidal": "live/midi/nova/lounge/suns_of_gold.tidal",
"take": "Take65",
"gig": "2025/la-french-stack",
"take_type": "SET",
"bpm": 94,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/ambient/contre_visite.tidal",
"name": "Contre Visite",
"tidal": "live/midi/nova/ambient/contre_visite.tidal",
"take": "Take66",
"gig": "2025/la-french-stack",
"take_type": "SET",
"bpm": 90,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/breaks/bain_electrique.tidal",
"name": "Bain Électrique",
"tidal": "live/midi/nova/breaks/bain_electrique.tidal",
"take": "Take66",
"gig": "2025/la-french-stack",
"take_type": "SET",
"bpm": 128,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/nujazz/salut_nu.tidal",
"name": "Salut Nu",
"tidal": "live/midi/nova/nujazz/salut_nu.tidal",
"take": "Take66",
"gig": "2025/la-french-stack",
"take_type": "SET",
"bpm": 120,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/nujazz/cafe_tiede.tidal",
"name": "Café Tiède",
"tidal": "live/midi/nova/nujazz/cafe_tiede.tidal",
"take": "Take66",
"gig": "2025/la-french-stack",
"take_type": "SET",
"bpm": 125,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/force_motrice.tidal",
"name": "Force Motrice",
"tidal": "live/midi/nova/dnb/force_motrice.tidal",
"take": "Take66",
"gig": "2025/la-french-stack",
"take_type": "SET",
"bpm": 125,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/lounge/sunny_side_up.tidal",
"name": "Sunny Side Up",
"tidal": "live/midi/nova/lounge/sunny_side_up.tidal",
"take": "Take66",
"gig": "2025/la-french-stack",
"take_type": "SET",
"bpm": 120,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/venons_ensemble.tidal",
"name": "Venons Ensemble",
"tidal": "live/midi/nova/dnb/venons_ensemble.tidal",
"take": "Take66",
"gig": "2025/la-french-stack",
"take_type": "SET",
"bpm": 85,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/something_about_drums.tidal",
"name": "Something about Drums",
"tidal": "live/midi/nova/dnb/something_about_drums.tidal",
"take": "Take66",
"gig": "2025/la-french-stack",
"take_type": "SET",
"bpm": 160,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/permanence.tidal",
"name": "Permanence",
"tidal": "live/collab/raph/permanence.tidal",
"take": "Take66",
"gig": "2025/la-french-stack",
"take_type": "SET",
"bpm": 150,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/lounge/suns_of_gold.tidal",
"name": "L'Or Bleu",
"tidal": "live/midi/nova/lounge/suns_of_gold.tidal",
"take": "Take66",
"gig": "2025/la-french-stack",
"take_type": "SET",
"bpm": 94,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/ambient/quand_on_decolle.tidal",
"name": "Quand on Décolle",
"tidal": "live/midi/nova/ambient/quand_on_decolle.tidal",
"take": "Take70",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 120,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/lounge/fabuleux.tidal",
"name": "Fabuleux",
"tidal": "live/midi/nova/lounge/fabuleux.tidal",
"take": "Take70",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 93,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/breaks/madeleine_de_paris.tidal",
"name": "Paris",
"tidal": "live/midi/nova/breaks/madeleine_de_paris.tidal",
"take": "Take70",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 80,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/ccc/ghosts_in_the_toilets.tidal",
"name": "Ghosts in the T01l3ts",
"tidal": "live/collab/ccc/ghosts_in_the_toilets.tidal",
"take": "Take70",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 160,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/techno/techno_orage.tidal",
"name": "Nuit d'orage",
"tidal": "live/midi/nova/techno/techno_orage.tidal",
"take": "Take70",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 104,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/breaks/bain_electrique.tidal",
"name": "Bain électrique",
"tidal": "live/midi/nova/breaks/bain_electrique.tidal",
"take": "Take70",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 128,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/beatober/oct_16_haunted_house_insouciance.tidal",
"name": "La fin de l'insouciance",
"tidal": "live/midi/nova/beatober/oct_16_haunted_house_insouciance.tidal",
"take": "Take70",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 120,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/venons_ensemble.tidal",
"name": "Venons Ensemble",
"tidal": "live/midi/nova/dnb/venons_ensemble.tidal",
"take": "Take70",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 85,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/lounge/suns_of_gold.tidal",
"name": "L'Or Bleu",
"tidal": "live/midi/nova/lounge/suns_of_gold.tidal",
"take": "Take70",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 94,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/lounge/sunny_side_up.tidal",
"name": "Sunny Side Up",
"tidal": "live/midi/nova/lounge/sunny_side_up.tidal",
"take": "Take70",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 120,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/baba/sept1.tidal",
"name": "Premier Septembre",
"tidal": "live/collab/baba/sept1.tidal",
"take": "Take70",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 120,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/breaks/lady_perplexity.tidal",
"name": "Lady Perplexity",
"tidal": "live/midi/nova/breaks/lady_perplexity.tidal",
"take": "Take70",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 138,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/techno/ete_a_mauerpark.tidal",
"name": "L'été à Mauerpark",
"tidal": "live/midi/nova/techno/ete_a_mauerpark.tidal",
"take": "Take70",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 120,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/jeudrill.tidal",
"name": "Jeudi Drill",
"tidal": "live/collab/raph/jeudrill.tidal",
"take": "Take70",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 140,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/ambient/quand_on_decolle.tidal",
"name": "Quand on Décolle",
"tidal": "live/midi/nova/ambient/quand_on_decolle.tidal",
"take": "Take71",
"gig": "2025/cosmicfest",
"take_type": "empty",
"bpm": 120,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/lounge/fabuleux.tidal",
"name": "Fabuleux",
"tidal": "live/midi/nova/lounge/fabuleux.tidal",
"take": "Take71",
"gig": "2025/cosmicfest",
"take_type": "empty",
"bpm": 93,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/breaks/madeleine_de_paris.tidal",
"name": "Paris",
"tidal": "live/midi/nova/breaks/madeleine_de_paris.tidal",
"take": "Take71",
"gig": "2025/cosmicfest",
"take_type": "empty",
"bpm": 80,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/ccc/ghosts_in_the_toilets.tidal",
"name": "Ghosts in the T01l3ts",
"tidal": "live/collab/ccc/ghosts_in_the_toilets.tidal",
"take": "Take71",
"gig": "2025/cosmicfest",
"take_type": "empty",
"bpm": 160,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/techno/techno_orage.tidal",
"name": "Nuit d'orage",
"tidal": "live/midi/nova/techno/techno_orage.tidal",
"take": "Take71",
"gig": "2025/cosmicfest",
"take_type": "empty",
"bpm": 104,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/breaks/bain_electrique.tidal",
"name": "Bain électrique",
"tidal": "live/midi/nova/breaks/bain_electrique.tidal",
"take": "Take71",
"gig": "2025/cosmicfest",
"take_type": "empty",
"bpm": 128,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/beatober/oct_16_haunted_house_insouciance.tidal",
"name": "La fin de l'insouciance",
"tidal": "live/midi/nova/beatober/oct_16_haunted_house_insouciance.tidal",
"take": "Take71",
"gig": "2025/cosmicfest",
"take_type": "empty",
"bpm": 120,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/venons_ensemble.tidal",
"name": "Venons Ensemble",
"tidal": "live/midi/nova/dnb/venons_ensemble.tidal",
"take": "Take71",
"gig": "2025/cosmicfest",
"take_type": "empty",
"bpm": 85,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/lounge/suns_of_gold.tidal",
"name": "L'Or Bleu",
"tidal": "live/midi/nova/lounge/suns_of_gold.tidal",
"take": "Take71",
"gig": "2025/cosmicfest",
"take_type": "empty",
"bpm": 94,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/lounge/sunny_side_up.tidal",
"name": "Sunny Side Up",
"tidal": "live/midi/nova/lounge/sunny_side_up.tidal",
"take": "Take71",
"gig": "2025/cosmicfest",
"take_type": "empty",
"bpm": 120,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/baba/sept1.tidal",
"name": "Premier Septembre",
"tidal": "live/collab/baba/sept1.tidal",
"take": "Take71",
"gig": "2025/cosmicfest",
"take_type": "empty",
"bpm": 120,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/breaks/lady_perplexity.tidal",
"name": "Lady Perplexity",
"tidal": "live/midi/nova/breaks/lady_perplexity.tidal",
"take": "Take71",
"gig": "2025/cosmicfest",
"take_type": "empty",
"bpm": 138,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/techno/ete_a_mauerpark.tidal",
"name": "L'été à Mauerpark",
"tidal": "live/midi/nova/techno/ete_a_mauerpark.tidal",
"take": "Take71",
"gig": "2025/cosmicfest",
"take_type": "empty",
"bpm": 120,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/jeudrill.tidal",
"name": "Jeudi Drill",
"tidal": "live/collab/raph/jeudrill.tidal",
"take": "Take71",
"gig": "2025/cosmicfest",
"take_type": "empty",
"bpm": 140,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±1d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/ambient/quand_on_decolle.tidal",
"name": "Quand on Décolle",
"tidal": "live/midi/nova/ambient/quand_on_decolle.tidal",
"take": "Take72",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 120,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/lounge/fabuleux.tidal",
"name": "Fabuleux",
"tidal": "live/midi/nova/lounge/fabuleux.tidal",
"take": "Take72",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 93,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/breaks/madeleine_de_paris.tidal",
"name": "Paris",
"tidal": "live/midi/nova/breaks/madeleine_de_paris.tidal",
"take": "Take72",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 80,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/ccc/ghosts_in_the_toilets.tidal",
"name": "Ghosts in the T01l3ts",
"tidal": "live/collab/ccc/ghosts_in_the_toilets.tidal",
"take": "Take72",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 160,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/techno/techno_orage.tidal",
"name": "Nuit d'orage",
"tidal": "live/midi/nova/techno/techno_orage.tidal",
"take": "Take72",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 104,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/breaks/bain_electrique.tidal",
"name": "Bain électrique",
"tidal": "live/midi/nova/breaks/bain_electrique.tidal",
"take": "Take72",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 128,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/beatober/oct_16_haunted_house_insouciance.tidal",
"name": "La fin de l'insouciance",
"tidal": "live/midi/nova/beatober/oct_16_haunted_house_insouciance.tidal",
"take": "Take72",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 120,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/venons_ensemble.tidal",
"name": "Venons Ensemble",
"tidal": "live/midi/nova/dnb/venons_ensemble.tidal",
"take": "Take72",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 85,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/lounge/suns_of_gold.tidal",
"name": "L'Or Bleu",
"tidal": "live/midi/nova/lounge/suns_of_gold.tidal",
"take": "Take72",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 94,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/lounge/sunny_side_up.tidal",
"name": "Sunny Side Up",
"tidal": "live/midi/nova/lounge/sunny_side_up.tidal",
"take": "Take72",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 120,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/baba/sept1.tidal",
"name": "Premier Septembre",
"tidal": "live/collab/baba/sept1.tidal",
"take": "Take72",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 120,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/breaks/lady_perplexity.tidal",
"name": "Lady Perplexity",
"tidal": "live/midi/nova/breaks/lady_perplexity.tidal",
"take": "Take72",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 138,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/techno/ete_a_mauerpark.tidal",
"name": "L'été à Mauerpark",
"tidal": "live/midi/nova/techno/ete_a_mauerpark.tidal",
"take": "Take72",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 120,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/jeudrill.tidal",
"name": "Jeudi Drill",
"tidal": "live/collab/raph/jeudrill.tidal",
"take": "Take72",
"gig": "2025/cosmicfest",
"take_type": "SET",
"bpm": 140,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date±2d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/baba/sept1.tidal",
"name": "Sept1",
"tidal": "live/collab/baba/sept1.tidal",
"take": "Take80",
"gig": "2025/bunker",
"take_type": "track",
"bpm": 120,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±3d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/lounge/sunny_side_up.tidal",
"name": "Sunny",
"tidal": "live/midi/nova/lounge/sunny_side_up.tidal",
"take": "Take80",
"gig": "2025/bunker",
"take_type": "track",
"bpm": 120,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±3d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/techno/techno_orage.tidal",
"name": "Orage",
"tidal": "live/midi/nova/techno/techno_orage.tidal",
"take": "Take80",
"gig": "2025/bunker",
"take_type": "track",
"bpm": 104,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±3d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/force_motrice.tidal",
"name": "Force Motrice",
"tidal": "live/midi/nova/dnb/force_motrice.tidal",
"take": "Take80",
"gig": "2025/bunker",
"take_type": "track",
"bpm": 125,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±3d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/jane/drifting_soul.tidal",
"name": "Drifting Soul",
"tidal": "live/collab/jane/drifting_soul.tidal",
"take": "Take80",
"gig": "2025/bunker",
"take_type": "track",
"bpm": 80,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±3d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "copycat/because_its_there.tidal",
"name": "Because It's There",
"tidal": "copycat/because_its_there.tidal",
"take": "Take80",
"gig": "2025/bunker",
"take_type": "track",
"bpm": 110,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±3d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/techno/ere_de_jeu.tidal",
"name": "Ere de Jeu",
"tidal": "live/midi/nova/techno/ere_de_jeu.tidal",
"take": "Take80",
"gig": "2025/bunker",
"take_type": "track",
"bpm": 110,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±3d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/acidule.tidal",
"name": "Acidule",
"tidal": "live/collab/raph/acidule.tidal",
"take": "Take80",
"gig": "2025/bunker",
"take_type": "track",
"bpm": 135,
"state": "candidate-ambiguous",
"signals": {
"metadata": true,
"join": "date±3d"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/ambient/quand_on_decolle.tidal",
"name": "Quand on décolle",
"tidal": "live/midi/nova/ambient/quand_on_decolle.tidal",
"take": "Take89",
"gig": "2026/montreuil-algorave",
"take_type": "SET",
"bpm": 60,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/piment_bresilien.tidal",
"name": "Piment brésilien",
"tidal": "live/collab/raph/piment_bresilien.tidal",
"take": "Take89",
"gig": "2026/montreuil-algorave",
"take_type": "SET",
"bpm": 124,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/lounge/take_5_drops.tidal",
"name": "Take 5 Drops",
"tidal": "live/midi/nova/lounge/take_5_drops.tidal",
"take": "Take89",
"gig": "2026/montreuil-algorave",
"take_type": "SET",
"bpm": 124,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/lounge/sunny_side_up.tidal",
"name": "Super Sunny Side Up",
"tidal": "live/midi/nova/lounge/sunny_side_up.tidal",
"take": "Take89",
"gig": "2026/montreuil-algorave",
"take_type": "SET",
"bpm": 120,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/wap.tidal",
"name": "W.I.P. W.A.P.",
"tidal": "live/midi/nova/dnb/wap.tidal",
"take": "Take89",
"gig": "2026/montreuil-algorave",
"take_type": "SET",
"bpm": 133,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/plosive.tidal",
"name": "'Plosive",
"tidal": "live/midi/nova/dnb/plosive.tidal",
"take": "Take89",
"gig": "2026/montreuil-algorave",
"take_type": "SET",
"bpm": 160,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/jeudrill.tidal",
"name": "Jeudi Drill",
"tidal": "live/collab/raph/jeudrill.tidal",
"take": "Take89",
"gig": "2026/montreuil-algorave",
"take_type": "SET",
"bpm": 140,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/aria_sans_serif.tidal",
"name": "Aria Sans Serif",
"tidal": "live/collab/raph/aria_sans_serif.tidal",
"take": "Take89",
"gig": "2026/montreuil-algorave",
"take_type": "SET",
"bpm": 160,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/punkachien.tidal",
"name": "PunkAChien",
"tidal": "live/collab/raph/punkachien.tidal",
"take": "Take89",
"gig": "2026/montreuil-algorave",
"take_type": "SET",
"bpm": 170,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/dnb/liquid/you_my_sunshine.tidal",
"name": "You My Sunshine",
"tidal": "live/midi/nova/dnb/liquid/you_my_sunshine.tidal",
"take": "Take89",
"gig": "2026/montreuil-algorave",
"take_type": "SET",
"bpm": 144,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/raph/desire.tidal",
"name": "Desire",
"tidal": "live/collab/raph/desire.tidal",
"take": "Take89",
"gig": "2026/montreuil-algorave",
"take_type": "SET",
"bpm": 129,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/mousquetaires/blue_gold.tidal",
"name": "L'or Bleu",
"tidal": "live/collab/mousquetaires/blue_gold.tidal",
"take": "Take89",
"gig": "2026/montreuil-algorave",
"take_type": "SET",
"bpm": 124,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/collab/baba/sept1.tidal",
"name": "Premier Septembre",
"tidal": "live/collab/baba/sept1.tidal",
"take": "Take89",
"gig": "2026/montreuil-algorave",
"take_type": "SET",
"bpm": 120,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/techno/techno_orage.tidal",
"name": "Techno Orage",
"tidal": "live/midi/nova/techno/techno_orage.tidal",
"take": "Take89",
"gig": "2026/montreuil-algorave",
"take_type": "SET",
"bpm": 104,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
},
{
"track": "live/midi/nova/jazz/the_revolution_will_be_sampled.tidal",
"name": "La Révolution Sera Samplée",
"tidal": "live/midi/nova/jazz/the_revolution_will_be_sampled.tidal",
"take": "Take89",
"gig": "2026/montreuil-algorave",
"take_type": "SET",
"bpm": 114,
"state": "candidate",
"signals": {
"metadata": true,
"join": "date-exact"
},
"src": "derived:datejoin(site_tracklist×take_gig_map)",
"as_of": "2026-06-05"
}
]
}
\ No newline at end of file
# sample_tier: original|light|heavy|remix|voice|set rights: clear|review|needs-license|no-go decision: released|candidate|live-only|test
title,kind,year,plays,min,on_dsp,sample_tier,rights,decision,notes
Paroles D'Opal,track,2024,279,5.9,yes,,,released,already on DSP
Live@39c3 House of Tea Party,set,2025,206,93.5,no,set,no-go,live-only,live set recording
Live @Opal 2024 - ParVagues TidalCycles Livecoding Set,set,2024,198,75.9,no,set,no-go,live-only,live set recording
Live@38C3: Secret Toilet Rave 🪠,set,2025,153,14.4,no,set,no-go,live-only,live set recording
Automne Électrique,track,2020,138,3.5,yes,,,released,already on DSP
Live@38C3: House of Tea 🍵,set,2025,127,61.7,no,set,no-go,live-only,live set recording
I come from Cyberspace,track,2021,121,2.1,no,,,,
Nass Arrive,track,2021,106,3.7,yes,,,released,already on DSP
Live@39c3 Secret Toilet Rave🕺,set,2026,105,54.8,no,set,no-go,live-only,live set recording
CCC LIVE,set,2024,91,28.0,no,set,no-go,live-only,live set recording
Livecoding @ Opal 2025,set,2025,80,84.3,no,set,no-go,live-only,live set recording
Bonsoir Minneapolis,track,2021,69,5.5,yes,,,released,already on DSP
Perce-neige Printanier,track,2021,69,2.0,no,,,,
Live@38C3: Chaos Music Club 🪩,track,2025,54,11.4,no,,,,
Faith in Bass (feat. Sculpture),track,2022,53,2.2,no,,,,
Courant Alternatif,track,2020,51,2.9,yes,,,released,already on DSP
Perfect Party - Mason X Princess Superstar live remix,track,2025,48,8.3,no,remix,no-go,live-only,remix/cover — needs license
Café Glacé,track,2024,44,6.5,yes,,,released,already on DSP
Samedi au Paradis,set,2025,43,14.2,no,set,no-go,live-only,live set recording
Paroles D'Opal (instrumental),track,2024,43,5.9,yes,,,released,already on DSP
Samedi ? Interdit ! (feat. Macron),track,2020,42,5.5,no,voice,no-go,live-only,celebrity/political voice
Soleil Pourpre,track,2020,40,3.3,no,,,,
Live @Algolia CKO 2025,set,2025,39,35.9,no,set,no-go,live-only,live set recording
WIP - WAP (Cardi B remix),track,2025,37,4.5,no,remix,no-go,live-only,remix/cover — needs license
El Mundo Interior (toma uno),track,2024,31,4.8,no,,,test,looks like a test/demo
Atlantis,track,2023,31,4.1,yes,,,released,already on DSP
Lavabo Noir,track,2021,31,2.7,yes,,,released,already on DSP
Rappel : Paroles d'Opal,set,2025,30,12.9,yes,,,released,already on DSP
Pixels Roses,track,2021,30,3.1,no,,,,
"Live @ Algorave | Ground Zero, 2025",set,2025,29,29.8,no,set,no-go,live-only,live set recording
Jardin d'Hiver,track,2021,29,2.7,no,,,,
Tidal Crime Investigation,track,2021,29,6.7,no,,,,
Au revoir Lord Toyota,track,2020,29,3.7,no,,,,
Vie Hivernale,track,2020,27,4.0,no,,,,
"Progressivement, Mardi",track,2020,25,6.3,no,,,,
Maudite soit la Guerre,track,2025,23,4.2,no,,,,
Live@algolia: Latin Heritage Party 🎈,set,2025,23,46.7,no,set,no-go,live-only,live set recording
Fabuleux,track,2024,23,4.5,yes,,,released,already on DSP
Stack Aérienne,track,2020,22,4.7,no,,,,
Premier Septembre (version longue),track,2025,21,7.2,no,,,,
Un dimanche mineur,track,2021,21,2.6,no,,,,
Live@Montreuil Algorave III,set,2026,20,77.3,no,set,no-go,live-only,live set recording
Live@cosmicfest 🌊🌅,set,2025,20,89.8,no,set,no-go,live-only,live set recording
Liquid Finale - feat @NVZT,track,2025,19,5.9,no,,,,
Ré comme remède,track,2020,17,5.6,no,,,,
Calorifère,track,2020,17,2.9,no,,,,
Du Code,track,2020,17,3.8,no,,,,
Transition à Fez : Ton numéro,track,2025,16,6.0,no,,,,
Ton Numéro,track,2025,16,4.6,no,,,,
Deck the (Dance) Hall,track,2024,15,2.1,no,,,,
Boeuf Confiné,set,2020,15,57.9,no,set,no-go,live-only,live set recording
Octobre Jaune,track,2023,14,5.3,no,,,,
Ciel Étoilé,track,2020,14,2.7,no,,,,
Live@ENSAD: Ambient to Techno,set,2025,13,53.6,no,set,no-go,live-only,live set recording
La Canopée,track,2023,13,4.5,no,,,,
Accel,track,2020,13,2.7,no,,,,
Battements Printaniers,track,2020,11,3.9,no,,,,
It's About Time,track,2023,10,5.5,no,,,,
Chez Cricri - Épisode 1 : Intro Citare,track,2025,9,2.8,no,,,,
Smart Motivated Person,track,2023,9,5.2,no,,,,
Dure Liberté,track,2020,9,4.1,no,,,,
Prière Dure,track,2020,8,8.6,no,,,,
Bleu Cassé,track,2020,7,4.1,no,,,,
YouMay,track,2020,7,4.4,no,,,,
Take Five Drops,track,2026,5,3.0,no,,,test,looks like a test/demo
Piment Brésilien,track,2026,4,3.0,no,,,,
Pensif,track,2020,4,5.0,no,,,,
Live@ENSAD: Bonus Encore,set,2025,3,23.2,no,set,no-go,live-only,live set recording
<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>L'Armada · Track Triage</title>
<style>
:root{--abyss:#06121c;--deep:#0b2233;--ink:#dff1f5;--mute:#8fb3c0;--line:#1d4358;--foam:#7fe3d4}
*{box-sizing:border-box}body{margin:0;font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;color:var(--ink);background:linear-gradient(180deg,var(--abyss),#02161f);height:100vh;overflow:hidden}
.app{display:grid;grid-template-columns:320px 1fr;height:100vh}
.side{border-right:1px solid var(--line);overflow:auto;padding:10px}
.side h1{font-size:14px;letter-spacing:1px;color:var(--foam);margin:6px 8px 4px}
.prog{font-size:12px;color:var(--mute);margin:0 8px 8px}
.bar{height:6px;background:#0a2c3c;border-radius:4px;margin:0 8px 10px;overflow:hidden}
.bar i{display:block;height:100%;background:var(--foam);transition:width .3s}
.row{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:7px;cursor:pointer;font-size:13px}
.row:hover{background:#0e2a3a}.row.cur{background:#12384c;outline:1px solid var(--foam)}
.dot{width:9px;height:9px;border-radius:50%;flex:0 0 auto;background:#26485a}
.row .ti{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}
.row .yr{color:var(--mute);font-size:11px}
.main{display:flex;flex-direction:column;padding:22px 26px;overflow:auto}
.crumbs{color:var(--mute);font-size:12px}
.title{font-size:26px;font-weight:700;margin:4px 0 2px}
.meta{color:var(--mute);font-size:13px;margin-bottom:12px}
.auto{display:inline-block;font-size:11px;border:1px solid var(--line);border-radius:6px;padding:1px 7px;color:var(--mute);margin-left:8px}
iframe{width:100%;height:120px;border:0;border-radius:10px;background:#0a2c3c;margin-bottom:16px}
.tiers{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px;max-width:760px}
.tier{display:flex;align-items:center;gap:10px;border:1px solid var(--line);border-radius:10px;padding:9px 11px;cursor:pointer;background:var(--deep);transition:.12s}
.tier:hover{border-color:var(--foam)}.tier.sel{outline:2px solid var(--foam);background:#103246}
.kbd{font-weight:800;width:22px;height:22px;border-radius:5px;display:grid;place-items:center;background:#0a2c3c;color:var(--ink);font-size:12px;flex:0 0 auto}
.tier .lab{font-size:13px}.tier .ds{font-size:11px;color:var(--mute)}
.note{margin-top:14px;max-width:760px}
.note input{width:100%;background:#0a2c3c;border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:9px 11px;font:inherit}
.hint{color:var(--mute);font-size:12px;margin-top:14px;max-width:760px}
.hint kbd{background:#0a2c3c;border:1px solid var(--line);border-radius:4px;padding:0 5px}
.tools{margin-top:auto;padding-top:16px;display:flex;gap:10px}
button{background:transparent;border:1px solid var(--line);color:var(--foam);border-radius:8px;padding:7px 13px;cursor:pointer;font:inherit}
button:hover{border-color:var(--foam)}
</style></head><body>
<div class="app">
<div class="side">
<h1>⛵ TRACK TRIAGE</h1>
<div class="prog" id="prog"></div><div class="bar"><i id="barfill"></i></div>
<div id="list"></div>
</div>
<div class="main">
<div class="crumbs" id="crumbs"></div>
<div class="title" id="title"></div>
<div class="meta" id="meta"></div>
<iframe id="player" allow="autoplay"></iframe>
<div class="tiers" id="tiers"></div>
<div class="note"><input id="note" placeholder="note (optional) — what's sampled, who's feat., etc."></div>
<div class="hint">Keys: <kbd></kbd><kbd></kbd> navigate · letter = tag &amp; advance · <kbd>Space</kbd> play/pause · <kbd>n</kbd> note · <kbd>Backspace</kbd> clear · auto-tags are dim until you confirm.</div>
<div class="tools">
<button onclick="expJSON()">⬇ Export JSON</button>
<button onclick="expCSV()">⬇ Export CSV</button>
<button onclick="if(confirm('Reset all annotations?')){localStorage.removeItem(KEY);location.reload()}">Reset</button>
</div>
</div>
</div>
<script src="https://w.soundcloud.com/player/api.js"></script>
<script>
const TRACKS=[{"t": "Paroles D'Opal", "u": "https://soundcloud.com/parvagues/paroles-d-opal", "y": "2024", "p": 279, "m": 5.9, "a": "released"}, {"t": "Automne Électrique", "u": "https://soundcloud.com/parvagues/automne-electrique", "y": "2020", "p": 138, "m": 3.5, "a": "released"}, {"t": "I come from Cyberspace", "u": "https://soundcloud.com/parvagues/i-come-from-cyberspace", "y": "2021", "p": 121, "m": 2.1, "a": ""}, {"t": "Nass Arrive", "u": "https://soundcloud.com/parvagues/nass-arrive", "y": "2021", "p": 106, "m": 3.7, "a": "released"}, {"t": "Bonsoir Minneapolis", "u": "https://soundcloud.com/parvagues/bonsoir-minneapolis", "y": "2021", "p": 69, "m": 5.5, "a": "released"}, {"t": "Perce-neige Printanier", "u": "https://soundcloud.com/parvagues/perceneige", "y": "2021", "p": 69, "m": 2.0, "a": ""}, {"t": "Live@38C3: Chaos Music Club 🪩", "u": "https://soundcloud.com/parvagues/live-38c3-chaos-music-club", "y": "2025", "p": 54, "m": 11.4, "a": ""}, {"t": "Faith in Bass (feat. Sculpture)", "u": "https://soundcloud.com/parvagues/faith-in-bass-feat-sculpture", "y": "2022", "p": 53, "m": 2.2, "a": ""}, {"t": "Courant Alternatif", "u": "https://soundcloud.com/parvagues/courant-alternatif", "y": "2020", "p": 51, "m": 2.9, "a": "released"}, {"t": "Perfect Party - Mason X Princess Superstar live remix", "u": "https://soundcloud.com/parvagues/perfect-party", "y": "2025", "p": 48, "m": 8.3, "a": "remix"}, {"t": "Café Glacé", "u": "https://soundcloud.com/parvagues/cafe-glace", "y": "2024", "p": 44, "m": 6.5, "a": "released"}, {"t": "Paroles D'Opal (instrumental)", "u": "https://soundcloud.com/parvagues/paroles-d-opal-instrumental", "y": "2024", "p": 43, "m": 5.9, "a": "released"}, {"t": "Samedi ? Interdit ! (feat. Macron)", "u": "https://soundcloud.com/parvagues/samedi-interdit", "y": "2020", "p": 42, "m": 5.5, "a": "voice"}, {"t": "Soleil Pourpre", "u": "https://soundcloud.com/parvagues/soleil-pourpre", "y": "2020", "p": 40, "m": 3.3, "a": ""}, {"t": "WIP - WAP (Cardi B remix)", "u": "https://soundcloud.com/parvagues/wip-wap", "y": "2025", "p": 37, "m": 4.5, "a": "remix"}, {"t": "El Mundo Interior (toma uno)", "u": "https://soundcloud.com/parvagues/el-mundo-interior", "y": "2024", "p": 31, "m": 4.8, "a": "test"}, {"t": "Atlantis", "u": "https://soundcloud.com/parvagues/atlantis", "y": "2023", "p": 31, "m": 4.1, "a": "released"}, {"t": "Lavabo Noir", "u": "https://soundcloud.com/parvagues/lavabo-noir", "y": "2021", "p": 31, "m": 2.7, "a": "released"}, {"t": "Rappel : Paroles d'Opal", "u": "https://soundcloud.com/parvagues/rappel-paroles-dopal-2", "y": "2025", "p": 30, "m": 12.9, "a": "released"}, {"t": "Pixels Roses", "u": "https://soundcloud.com/parvagues/pixels-roses", "y": "2021", "p": 30, "m": 3.1, "a": ""}, {"t": "Jardin d'Hiver", "u": "https://soundcloud.com/parvagues/jardin-dhiver", "y": "2021", "p": 29, "m": 2.7, "a": ""}, {"t": "Tidal Crime Investigation", "u": "https://soundcloud.com/parvagues/tidal-crime", "y": "2021", "p": 29, "m": 6.7, "a": ""}, {"t": "Au revoir Lord Toyota", "u": "https://soundcloud.com/parvagues/au-revoir-lord-toyota", "y": "2020", "p": 29, "m": 3.7, "a": ""}, {"t": "Vie Hivernale", "u": "https://soundcloud.com/parvagues/vie-hivernale", "y": "2020", "p": 27, "m": 4.0, "a": ""}, {"t": "Progressivement, Mardi", "u": "https://soundcloud.com/parvagues/progressivement-mardi", "y": "2020", "p": 25, "m": 6.3, "a": ""}, {"t": "Maudite soit la Guerre", "u": "https://soundcloud.com/parvagues/maudite-soit-la-guerre", "y": "2025", "p": 23, "m": 4.2, "a": ""}, {"t": "Fabuleux", "u": "https://soundcloud.com/parvagues/fabuleux", "y": "2024", "p": 23, "m": 4.5, "a": "released"}, {"t": "Stack Aérienne", "u": "https://soundcloud.com/parvagues/stack-aerienne", "y": "2020", "p": 22, "m": 4.7, "a": ""}, {"t": "Premier Septembre (version longue)", "u": "https://soundcloud.com/parvagues/premier-septembre", "y": "2025", "p": 21, "m": 7.2, "a": ""}, {"t": "Un dimanche mineur", "u": "https://soundcloud.com/parvagues/un-dimanche-mineur", "y": "2021", "p": 21, "m": 2.6, "a": ""}, {"t": "Liquid Finale - feat @NVZT", "u": "https://soundcloud.com/parvagues/liquid-finale", "y": "2025", "p": 19, "m": 5.9, "a": ""}, {"t": "Ré comme remède", "u": "https://soundcloud.com/parvagues/re-comme-remede", "y": "2020", "p": 17, "m": 5.6, "a": ""}, {"t": "Calorifère", "u": "https://soundcloud.com/parvagues/calorifere", "y": "2020", "p": 17, "m": 2.9, "a": ""}, {"t": "Du Code", "u": "https://soundcloud.com/parvagues/du-code", "y": "2020", "p": 17, "m": 3.8, "a": ""}, {"t": "Transition à Fez : Ton numéro", "u": "https://soundcloud.com/parvagues/transition-to-jam-de-fez-ton-numero-3", "y": "2025", "p": 16, "m": 6.0, "a": ""}, {"t": "Ton Numéro", "u": "https://soundcloud.com/parvagues/ton-numero", "y": "2025", "p": 16, "m": 4.6, "a": ""}, {"t": "Deck the (Dance) Hall", "u": "https://soundcloud.com/parvagues/deck-the-dance-hall", "y": "2024", "p": 15, "m": 2.1, "a": ""}, {"t": "Octobre Jaune", "u": "https://soundcloud.com/parvagues/octobre-jaune", "y": "2023", "p": 14, "m": 5.3, "a": ""}, {"t": "Ciel Étoilé", "u": "https://soundcloud.com/parvagues/ciel-etoile", "y": "2020", "p": 14, "m": 2.7, "a": ""}, {"t": "La Canopée", "u": "https://soundcloud.com/parvagues/la-canopee", "y": "2023", "p": 13, "m": 4.5, "a": ""}, {"t": "Accel", "u": "https://soundcloud.com/parvagues/accel", "y": "2020", "p": 13, "m": 2.7, "a": ""}, {"t": "Battements Printaniers", "u": "https://soundcloud.com/parvagues/battements-printaniers", "y": "2020", "p": 11, "m": 3.9, "a": ""}, {"t": "It's About Time", "u": "https://soundcloud.com/parvagues/its-about-time", "y": "2023", "p": 10, "m": 5.5, "a": ""}, {"t": "Chez Cricri - Épisode 1 : Intro Citare", "u": "https://soundcloud.com/parvagues/chez-cricri1", "y": "2025", "p": 9, "m": 2.8, "a": ""}, {"t": "Smart Motivated Person", "u": "https://soundcloud.com/parvagues/smart-motivated-person", "y": "2023", "p": 9, "m": 5.2, "a": ""}, {"t": "Dure Liberté", "u": "https://soundcloud.com/parvagues/dure-liberte", "y": "2020", "p": 9, "m": 4.1, "a": ""}, {"t": "Prière Dure", "u": "https://soundcloud.com/parvagues/priere-dure", "y": "2020", "p": 8, "m": 8.6, "a": ""}, {"t": "Bleu Cassé", "u": "https://soundcloud.com/parvagues/bleucasse", "y": "2020", "p": 7, "m": 4.1, "a": ""}, {"t": "YouMay", "u": "https://soundcloud.com/parvagues/youmay-feat-youc", "y": "2020", "p": 7, "m": 4.4, "a": ""}, {"t": "Take Five Drops", "u": "https://soundcloud.com/parvagues/take-five-drops-2", "y": "2026", "p": 5, "m": 3.0, "a": "test"}, {"t": "Piment Brésilien", "u": "https://soundcloud.com/parvagues/piment-bresilien-1", "y": "2026", "p": 4, "m": 3.0, "a": ""}, {"t": "Pensif", "u": "https://soundcloud.com/parvagues/pensif", "y": "2020", "p": 4, "m": 5.0, "a": ""}];
const TIERS=[
{k:'o',id:'original',lab:'Original',c:'#46d39a',ds:'✅ release'},
{k:'l',id:'light',lab:'Light sampling',c:'#7fe3d4',ds:'✅ low-risk'},
{k:'h',id:'heavy',lab:'Heavy sampling',c:'#ffc24d',ds:'🟡 clear first'},
{k:'r',id:'remix',lab:'Remix / Cover',c:'#ff7a6b',ds:'🔴 live-only'},
{k:'v',id:'voice',lab:'Voice / Satire',c:'#ff6b6b',ds:'🔴 live-only'},
{k:'s',id:'set',lab:'Live-set',c:'#8fb3c0',ds:'🔴 live-only'},
{k:'t',id:'test',lab:'Test / Sketch',c:'#6a8290',ds:'— shelf'},
{k:'x',id:'released',lab:'Released',c:'#4a6b7a',ds:'skip'},
];
const TBK=Object.fromEntries(TIERS.map(t=>[t.k,t.id]));
const TBI=Object.fromEntries(TIERS.map(t=>[t.id,t]));
const KEY='armada_triage_v1';
let state=JSON.parse(localStorage.getItem(KEY)||'null');
if(!state){state={};TRACKS.forEach(t=>{if(t.a)state[t.u]={tier:t.a,note:'',auto:true};});}
let i=0,widget=null;
const $=id=>document.getElementById(id);
function save(){localStorage.setItem(KEY,JSON.stringify(state));}
function done(){return TRACKS.filter(t=>state[t.u]&&!state[t.u].auto).length;}
function loadPlayer(u){
const f=$('player');
f.src='https://w.soundcloud.com/player/?url='+encodeURIComponent(u)+'&color=%2339c2b0&auto_play=false&show_comments=false&visual=false&buying=false&sharing=false&download=false';
try{widget=SC.Widget(f);}catch(e){widget=null;}
}
function renderList(){
const L=$('list');L.innerHTML='';
TRACKS.forEach((t,n)=>{
const a=state[t.u];const col=a?TBI[a.tier].c:'#26485a';
const d=document.createElement('div');d.className='row'+(n===i?' cur':'');
d.innerHTML='<span class="dot" style="background:'+col+(a&&a.auto?';opacity:.45':'')+'"></span><span class="ti">'+t.t+'</span><span class="yr">'+t.y+'</span>';
d.onclick=()=>{i=n;render();};L.appendChild(d);
});
}
function render(){
const t=TRACKS[i];const a=state[t.u];
$('crumbs').textContent=(i+1)+' / '+TRACKS.length+(t.a?' · auto-guess: '+TBI[t.a].lab:'');
$('title').textContent=t.t;
$('meta').innerHTML=t.y+' · '+t.p+' plays · '+t.m+' min'+(a?' <span class="auto">'+(a.auto?'auto: ':'you: ')+TBI[a.tier].lab+'</span>':'');
const T=$('tiers');T.innerHTML='';
TIERS.forEach(tr=>{
const sel=a&&a.tier===tr.id;
const e=document.createElement('div');e.className='tier'+(sel?' sel':'');
e.style.borderLeft='4px solid '+tr.c;
e.innerHTML='<span class="kbd">'+tr.k.toUpperCase()+'</span><span><div class="lab">'+tr.lab+'</div><div class="ds">'+tr.ds+'</div></span>';
e.onclick=()=>tag(tr.id);T.appendChild(e);
});
$('note').value=a?a.note||'':'';
const d=done();$('prog').textContent=d+' / '+TRACKS.length+' confirmed ('+(TRACKS.length-d)+' to go)';
$('barfill').style.width=(100*d/TRACKS.length)+'%';
loadPlayer(t.u);renderList();
document.querySelector('.row.cur')?.scrollIntoView({block:'nearest'});
}
function tag(id){const t=TRACKS[i];state[t.u]={tier:id,note:($('note').value||''),auto:false};save();nextTodo();}
function nextTodo(){let j=i;for(let n=1;n<=TRACKS.length;n++){const k=(i+n)%TRACKS.length;const a=state[TRACKS[k].u];if(!a||a.auto){j=k;break;}}i=(j===i)?Math.min(i+1,TRACKS.length-1):j;render();}
function go(d){i=Math.max(0,Math.min(TRACKS.length-1,i+d));render();}
document.addEventListener('keydown',e=>{
if(e.target.id==='note'){if(e.key==='Enter'||e.key==='Escape'){state[TRACKS[i].u]=state[TRACKS[i].u]||{tier:'',auto:false};state[TRACKS[i].u].note=$('note').value;save();e.target.blur();}return;}
if(e.key==='ArrowRight'){go(1);e.preventDefault();}
else if(e.key==='ArrowLeft'){go(-1);e.preventDefault();}
else if(e.key===' '){if(widget)widget.toggle();e.preventDefault();}
else if(e.key==='n'){$('note').focus();e.preventDefault();}
else if(e.key==='Backspace'){delete state[TRACKS[i].u];save();render();e.preventDefault();}
else if(TBK[e.key.toLowerCase()]){tag(TBK[e.key.toLowerCase()]);e.preventDefault();}
});
function dl(name,txt,type){const b=new Blob([txt],{type});const u=URL.createObjectURL(b);const a=document.createElement('a');a.href=u;a.download=name;a.click();URL.revokeObjectURL(u);}
function expJSON(){const o=TRACKS.map(t=>({title:t.t,url:t.u,year:t.y,plays:t.p,min:t.m,tier:state[t.u]?.tier||'',dsp:state[t.u]?TBI[state[t.u].tier].ds:'',confirmed:state[t.u]?!state[t.u].auto:false,note:state[t.u]?.note||''}));dl('triage-export.json',JSON.stringify(o,null,2),'application/json');}
function expCSV(){const rows=[['title','url','tier','dsp','confirmed','note']];TRACKS.forEach(t=>{const a=state[t.u];rows.push([t.t,t.u,a?.tier||'',a?TBI[a.tier].ds:'',a?(!a.auto):'',a?.note||''].map(x=>'"'+String(x).replace(/"/g,'""')+'"'));});dl('triage-export.csv',rows.map(r=>r.join(',')).join('\n'),'text/csv');}
render();
</script></body></html>
\ No newline at end of file
node_modules
dist
*.log
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
globals: globals.browser,
},
},
])
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ui</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "ui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@fontsource-variable/geist": "^5.2.9",
"@fontsource-variable/geist-mono": "^5.2.8",
"@wavesurfer/react": "^1.0.12",
"clsx": "^2.1.1",
"lucide-react": "^1.17.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"wavesurfer.js": "^7.12.7"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@tailwindcss/vite": "^4.3.0",
"@types/node": "^24.12.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^10.3.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
"playwright": "^1.60.0",
"tailwindcss": "^4.3.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.59.2",
"vite": "^8.0.12"
}
}
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>
{"track":"PunkAChien","calibration":"stem ≈ master − 3 s (verified by cross-correlation, z=29)","activeDb":-38.0,"roleGroups":[{"key":"percs","label":"Percs","color":"#ff8c00","glyph":"▰"},{"key":"bass","label":"Bass","color":"#7c5cff","glyph":"▂"},{"key":"melodic","label":"Melodic","color":"#36c5f0","glyph":"♪"},{"key":"tops","label":"Tops","color":"#2dd4bf","glyph":"≈"},{"key":"atmos","label":"Atmos","color":"#8a93a6","glyph":"◌"}],"note":"Single confirmed take (Montreuil). Hamburg/39C3 was misidentified (actually Insouciance→Liquid Finale) and pulled; real Hamburg PunkAChien = 38C3 Toilet 2024, pending re-source.","takes":[{"id":"montreuil","label":"Montreuil","gig":"Montreuil Algorave V3 — Mai Floral","date":"2026-05-22","dur":287.74,"binS":2.0,"masters":{"stream":{"file":"masters/montreuil_stream_v1.flac","I":-14.0,"LRA":6.4,"TP":-1.0},"club":{"file":"masters/montreuil_club_v1.flac","I":-11.4,"LRA":3.8,"TP":-0.7}},"loud":{"stream":{"trace":[-80.2,-17.8,-15.9,-13.3,-12.9,-11.6,-13.9,-12.3,-11.0,-11.1,-13.9,-13.5,-15.6,-14.4,-12.8,-14.7,-13.9,-16.5,-17.7,-13.2,-10.2,-12.8,-13.4,-19.2,-16.7,-14.5,-17.5,-20.9,-19.4,-21.6,-18.3,-16.1,-18.8,-17.8,-19.4,-20.0,-17.2,-15.4,-18.3,-16.2,-19.2,-16.5,-13.7,-12.1,-12.8,-14.0,-16.9,-14.0,-12.1,-14.2,-12.3,-14.1,-12.7,-10.2,-10.5,-14.1,-12.0,-16.3,-14.6,-11.4,-13.8,-13.2,-13.4,-14.9,-13.0,-11.6,-13.3,-15.2,-15.4,-15.2,-14.3,-14.0,-14.0,-13.9,-14.9,-14.4,-13.4,-14.6,-14.4,-14.7,-15.7,-13.1,-12.4,-13.0,-11.3,-12.1,-11.7,-10.7,-11.1,-14.1,-12.9,-14.5,-14.0,-12.5,-14.1,-12.8,-13.4,-15.0,-11.4,-10.3,-12.0,-11.6,-12.5,-12.4,-11.3,-11.3,-12.3,-11.0,-12.5,-11.3,-9.5,-12.9,-13.3,-14.6,-15.2,-13.1,-12.7,-14.6,-13.4,-15.0,-14.4,-13.0,-12.2,-13.8,-14.0,-16.0,-13.8,-12.1,-13.1,-12.2,-12.9,-13.7,-13.8,-13.9,-11.9,-11.7,-13.8,-12.9,-12.1,-13.1,-12.5,-12.7,-14.0,-11.6,-11.2,-11.7,-12.0,-13.2,-13.5,-12.1,-12.7,-13.2,-12.4,-13.8,-13.0,-11.8,-13.9,-18.1,-19.4,-21.9,-18.4,-14.9,-16.4,-14.6,-15.3,-15.5,-13.4,-12.3,-15.0,-14.1,-16.2,-14.2,-12.5,-12.9,-13.1,-12.9,-21.3,-21.7,-24.1,-17.9,-16.7,-16.1,-15.3,-14.8,-15.4,-15.4,-15.0,-14.6,-15.2,-15.1,-14.2,-15.0,-15.9,-16.2,-15.2,-15.5,-16.3,-16.3,-14.7,-17.1,-18.2,-14.0,-14.8,-15.4,-15.9,-15.3,-14.1,-14.7,-14.1,-15.1,-15.7,-14.0,-13.0,-14.8,-14.2,-16.1,-15.3,-13.9,-14.3,-14.8,-14.4,-16.2,-12.5,-10.9,-12.7,-13.8,-15.0,-16.9,-13.9,-14.5,-16.6,-13.7,-16.0,-13.8,-12.6,-14.7,-14.5,-13.3,-16.8,-14.2,-13.7,-17.5,-14.1,-14.7,-16.8,-13.4,-13.0,-15.3,-16.2,-17.1,-16.9,-15.8,-14.7,-14.5,-14.3,-13.8,-13.5,-13.1,-11.9,-13.2,-13.6,-13.5,-13.4,-13.2,-13.8,-13.9,-16.1,-26.2,-30.6,-20.1,-12.8,-13.3,-13.8,-12.8,-13.1,-13.5,-12.8,-13.9,-13.3,-12.3,-12.2,-12.4,-12.9,-13.3,-13.0,-12.7,-13.3,-12.6,-12.7,-16.5,-21.0,-23.1,-12.7,-12.5,-13.0,-12.8,-12.7,-13.0,-13.2,-13.2,-13.2,-14.6,-20.0,-17.1,-13.3,-13.5,-13.8,-13.5,-13.3,-13.4,-13.3,-13.9,-14.7,-13.8,-12.5,-12.6,-14.6,-19.2,-16.0,-15.2,-17.8,-15.2,-17.2,-16.5,-16.1,-19.6,-21.5,-19.7,-21.7,-20.6,-18.1,-19.7,-20.3,-20.5,-21.3,-19.0,-16.8,-15.5,-17.3,-17.9,-17.5,-15.3,-14.5,-16.2,-15.0,-16.5,-16.9,-16.2,-15.5,-19.1,-19.7,-20.7,-15.9,-14.4,-16.5,-15.1,-16.8,-16.4,-13.7,-12.2,-12.4,-12.7,-14.6,-13.0,-12.6,-13.7,-12.7,-13.6,-14.5,-11.5,-11.7,-12.6,-12.6,-15.0,-14.2,-12.7,-13.8,-21.2,-22.4,-22.8,-25.4,-24.9,-16.8,-15.9,-18.8,-21.4,-18.2,-17.1,-20.1,-18.2,-20.8,-20.0,-17.4,-16.7,-19.7,-18.5,-21.5,-18.8,-17.4,-19.5,-18.3,-19.6,-18.8,-14.6,-12.9,-15.1,-13.4,-14.7,-13.9,-13.5,-14.1,-13.6,-13.7,-14.8,-13.0,-12.6,-13.5,-13.5,-13.6,-15.6,-13.2,-13.8,-15.0,-13.3,-15.9,-14.3,-11.8,-12.6,-16.5,-16.5,-15.4,-13.0,-12.7,-13.8,-13.1,-13.4,-14.3,-14.5,-13.7,-13.7,-12.9,-13.7,-13.5,-12.0,-13.6,-13.1,-12.9,-13.0,-14.2,-13.6,-12.6,-13.7,-14.1,-14.5,-13.9,-13.6,-14.0,-14.5,-13.8,-15.4,-15.0,-13.0,-13.3,-14.5,-14.4,-13.8,-13.7,-15.0,-15.8,-16.2,-14.6,-14.8,-13.2,-13.5,-14.4,-13.8,-14.3,-13.8,-14.1,-13.6,-14.3,-14.6,-15.3,-14.0,-13.1,-14.0,-13.7,-14.0,-13.7,-13.1,-13.5,-14.0,-13.7,-15.2,-14.1,-15.6,-18.6,-19.5,-18.5,-17.5,-18.2,-18.2,-18.2,-18.0,-18.1,-17.8,-17.7,-17.8,-18.8,-19.0,-17.1,-17.9,-18.4,-19.2,-18.6,-18.6,-16.2,-12.6,-14.3,-15.2,-16.2,-13.9,-13.2,-13.6,-13.9,-14.5,-15.3,-13.9,-12.4,-13.9,-14.1,-15.2,-14.6,-13.7,-13.1,-14.5,-14.7,-16.9,-15.1,-13.2,-10.7,-10.6,-10.6,-10.2,-10.8,-10.1,-10.0,-11.3,-11.2,-11.1,-11.4,-11.5,-11.4,-10.5,-10.6,-10.5,-10.4,-9.5,-11.5,-11.8,-10.3,-11.0,-11.2,-17.7,-18.3,-22.1,-21.6,-18.7,-18.9,-19.4,-19.8,-27.0,-26.5,-27.9],"stepS":0.5},"club":{"trace":[-77.4,-10.9,-10.2,-8.3,-10.1,-11.4,-11.7,-9.4,-8.7,-9.2,-10.8,-12.5,-11.4,-11.9,-13.6,-12.6,-12.1,-13.0,-12.9,-11.9,-10.2,-12.0,-9.3,-12.5,-10.7,-9.1,-11.8,-14.0,-12.4,-14.7,-11.9,-9.5,-12.5,-11.5,-12.4,-13.3,-10.8,-9.3,-11.9,-10.1,-12.2,-10.8,-8.9,-9.1,-12.0,-11.0,-11.4,-12.0,-10.0,-11.5,-11.7,-11.5,-11.9,-10.0,-10.5,-11.5,-11.7,-11.9,-12.1,-10.6,-10.9,-11.0,-9.1,-11.1,-10.7,-10.3,-11.1,-12.4,-10.6,-12.9,-13.9,-10.2,-12.0,-11.4,-11.6,-12.3,-10.3,-11.0,-10.0,-9.5,-10.3,-11.7,-10.3,-10.6,-11.6,-12.4,-10.8,-9.5,-10.7,-10.9,-9.2,-11.9,-12.1,-11.8,-12.7,-12.2,-15.7,-14.6,-12.9,-13.5,-14.7,-11.9,-12.9,-13.2,-12.1,-13.2,-12.8,-11.5,-12.7,-12.0,-10.7,-15.1,-16.3,-11.7,-12.3,-13.1,-10.1,-11.7,-13.5,-11.4,-11.7,-13.1,-11.3,-10.4,-8.6,-9.5,-9.1,-9.9,-11.9,-10.8,-10.4,-12.1,-12.3,-12.5,-14.6,-15.7,-14.9,-12.9,-15.2,-15.5,-12.8,-15.4,-14.6,-11.9,-14.2,-14.4,-10.9,-11.6,-11.8,-11.7,-12.4,-11.4,-10.9,-11.9,-11.6,-12.0,-11.2,-11.4,-12.4,-14.8,-11.7,-9.2,-9.9,-9.1,-9.4,-9.2,-9.2,-9.8,-9.6,-9.6,-9.8,-9.7,-10.3,-11.1,-10.6,-11.2,-15.8,-16.7,-17.6,-13.0,-12.1,-13.7,-16.0,-12.4,-12.2,-14.3,-12.3,-13.5,-13.1,-10.8,-13.9,-13.3,-11.1,-13.4,-12.3,-10.5,-11.9,-10.1,-9.4,-10.7,-11.9,-10.3,-11.2,-9.4,-11.7,-10.6,-9.5,-10.8,-11.7,-12.1,-11.3,-12.8,-15.0,-10.7,-9.3,-9.8,-9.3,-9.2,-9.0,-9.2,-9.3,-9.5,-8.9,-9.5,-10.0,-12.1,-13.6,-13.2,-12.9,-12.3,-13.0,-12.5,-13.3,-11.4,-10.8,-9.6,-11.1,-12.2,-12.2,-12.0,-10.8,-11.3,-11.7,-12.5,-12.1,-10.7,-11.0,-11.7,-10.5,-11.3,-10.9,-9.5,-8.9,-9.7,-8.8,-11.8,-12.4,-11.5,-11.6,-13.0,-13.0,-12.3,-12.5,-11.5,-12.3,-10.5,-10.3,-19.3,-23.6,-16.8,-12.2,-12.0,-14.0,-12.7,-11.3,-13.1,-12.5,-12.9,-12.9,-15.0,-15.8,-16.7,-12.6,-12.1,-13.7,-13.8,-13.7,-12.4,-13.1,-14.4,-16.1,-16.8,-12.1,-12.2,-11.5,-11.9,-11.4,-10.6,-12.4,-11.6,-11.9,-11.1,-13.2,-13.1,-13.2,-12.0,-13.3,-13.6,-11.3,-12.8,-13.3,-12.8,-11.1,-9.4,-10.7,-15.2,-12.3,-13.3,-11.9,-10.5,-11.6,-11.3,-12.0,-10.6,-11.1,-12.9,-14.6,-12.8,-14.9,-14.3,-12.3,-13.4,-14.7,-14.0,-16.1,-13.3,-10.4,-10.1,-12.0,-12.8,-12.9,-11.2,-9.3,-11.2,-10.5,-12.6,-11.2,-10.1,-12.6,-12.8,-12.7,-13.7,-10.5,-9.0,-10.3,-9.9,-10.1,-10.3,-9.4,-9.4,-14.1,-16.3,-17.0,-12.7,-14.8,-13.3,-12.6,-17.1,-15.7,-12.2,-15.1,-16.0,-16.2,-15.4,-12.5,-14.2,-16.1,-18.0,-16.6,-19.1,-18.4,-17.9,-11.2,-10.5,-11.9,-14.5,-11.6,-10.2,-13.3,-11.5,-13.8,-13.4,-10.8,-10.4,-13.1,-11.6,-14.5,-12.3,-10.4,-12.6,-11.8,-12.6,-12.2,-8.9,-8.8,-9.7,-8.4,-11.6,-10.4,-8.8,-11.3,-10.0,-9.4,-12.3,-10.5,-12.1,-9.6,-10.8,-9.6,-11.7,-11.4,-9.4,-11.3,-10.6,-11.8,-12.2,-10.4,-10.3,-9.9,-10.3,-9.6,-9.2,-10.2,-9.7,-9.4,-9.8,-10.0,-11.9,-11.0,-9.3,-10.1,-9.8,-9.1,-10.2,-9.8,-9.5,-11.0,-10.5,-11.5,-10.6,-9.7,-11.2,-11.0,-12.4,-14.2,-10.9,-11.1,-12.2,-11.8,-13.8,-12.1,-12.7,-11.7,-11.5,-12.6,-13.8,-10.7,-11.4,-9.9,-10.2,-11.2,-12.3,-10.5,-10.4,-12.0,-11.8,-12.7,-12.5,-12.1,-11.0,-11.3,-12.7,-14.7,-10.1,-13.1,-11.4,-10.4,-11.9,-15.1,-11.1,-10.3,-10.6,-11.4,-13.1,-11.9,-12.9,-11.6,-12.5,-11.5,-10.5,-11.2,-11.5,-11.3,-11.2,-11.2,-11.0,-10.8,-11.0,-11.8,-12.1,-10.2,-10.9,-11.4,-12.2,-11.6,-11.6,-11.0,-10.6,-10.5,-10.2,-9.8,-11.3,-11.6,-9.8,-9.5,-10.7,-9.5,-9.9,-10.7,-12.3,-12.5,-11.7,-10.7,-12.0,-10.6,-9.4,-11.0,-10.7,-9.7,-8.6,-10.3,-12.2,-12.8,-12.5,-12.9,-13.4,-13.3,-14.2,-14.4,-14.2,-14.0,-14.0,-14.5,-13.6,-13.9,-13.3,-13.3,-13.0,-14.1,-14.6,-13.3,-12.3,-12.1,-12.8,-11.7,-15.1,-14.7,-11.7,-11.9,-12.9,-13.0,-20.1,-19.6,-20.9,-29.1],"stepS":0.5}},"orbits":[{"orbit":"01","label":"kick","group":"percs","activity":[-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-18.0,-21.0,-18.9,-22.4,-19.5,-16.6,-15.5,-15.9,-17.0,-240.0,-240.0,-22.5,-17.1,-16.5,-18.3,-240.0,-240.0,-17.0,-16.8,-16.5,-16.2,-12.6,-11.4,-11.1,-11.7,-10.7,-11.8,-11.7,-13.9,-240.0,-240.0,-240.0,-240.0,-240.0,-22.4,-15.4,-15.6,-14.6,-15.7,-16.3,-17.1,-17.7,-16.9,-17.3,-17.7,-23.0,-240.0,-240.0,-240.0,-240.0,-240.0,-24.7,-15.0,-12.4,-11.0,-13.2,-23.3,-11.7,-11.5,-11.2,-11.7,-12.2,-13.9,-11.7,-12.2,-13.9,-11.7,-14.5,-14.0,-240.0,-240.0,-240.0,-240.0,-18.9,-18.0,-17.9,-18.9,-240.0,-240.0,-14.7,-13.3,-13.1,-12.8,-15.5,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-23.7,-13.2,-13.6,-14.3,-17.5,-22.1,-19.8,-17.0,-19.0,-17.4,-17.4,-17.4,-15.1,-14.4,-14.1,-14.6,-14.4,-20.6,-14.6,-14.4,-13.9,-14.8,-14.4,-19.1,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-15.6,-11.3,-12.2,-12.3,-16.9,-19.0,-240.0,-240.0,-240.0,-240.0]},{"orbit":"02","label":"snare","group":"percs","activity":[-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-65.8,-29.7,-28.7,-28.9,-240.0,-240.0,-33.6,-26.8,-24.9,-23.8,-25.0,-25.0,-23.9,-26.6,-24.9,-26.9,-240.0,-33.5,-24.3,-26.6,-24.9,-240.0,-240.0,-30.2,-240.0,-240.0,-240.0,-240.0,-240.0,-26.9,-24.6,-25.9,-23.1,-26.1,-34.5,-26.9,-27.1,-28.6,-240.0,-240.0,-240.0,-48.1,-240.0,-240.0,-240.0,-240.0,-70.2,-70.3,-70.4,-28.4,-24.5,-27.6,-22.0,-22.2,-22.8,-240.0,-240.0,-240.0,-240.0,-240.0,-27.4,-28.9,-29.1,-29.1,-240.0,-240.0,-240.0,-240.0,-30.2,-26.0,-25.9,-26.2,-240.0,-240.0,-24.8,-22.3,-22.2,-24.7,-27.1,-32.7,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-106.3,-48.0,-33.7,-29.5,-34.2,-240.0,-240.0,-240.0,-240.0,-29.5,-24.8,-24.6,-24.6,-23.5,-24.6,-29.2,-29.7,-26.2,-24.6,-23.4,-24.7,-24.7,-27.1,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-49.1,-50.4,-30.9,-30.0,-31.6,-240.0,-240.0,-240.0,-240.0]},{"orbit":"03","label":"hats","group":"percs","activity":[-240.0,-240.0,-240.0,-240.0,-240.0,-75.9,-53.0,-48.1,-41.7,-240.0,-240.0,-240.0,-240.0,-44.0,-40.3,-40.3,-41.0,-40.6,-40.5,-37.7,-43.3,-41.0,-37.4,-37.3,-36.7,-37.7,-36.7,-37.2,-37.1,-36.7,-45.2,-133.8,-49.9,-36.2,-36.1,-36.5,-93.2,-142.3,-40.2,-240.0,-240.0,-240.0,-240.0,-240.0,-41.1,-39.2,-39.0,-39.7,-42.3,-55.8,-40.2,-40.6,-41.1,-103.7,-143.1,-44.3,-40.7,-40.2,-40.5,-40.7,-40.0,-41.5,-40.3,-40.3,-41.0,-40.6,-40.4,-40.5,-41.1,-41.4,-240.0,-240.0,-41.5,-41.9,-39.2,-39.1,-38.9,-43.3,-41.9,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-48.1,-39.1,-38.1,-37.8,-38.5,-37.4,-42.3,-46.0,-51.4,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-65.2,-56.4,-40.2,-40.0,-43.7,-76.6,-240.0,-240.0,-240.0,-240.0,-240.0,-44.0,-41.6,-41.5,-41.5,-47.8,-47.2,-42.0,-41.8,-41.3,-42.2,-41.1,-41.5,-37.8,-37.3,-38.3,-40.0,-115.4,-143.8,-240.0,-45.5,-37.8,-45.2,-41.7,-38.0,-37.7,-37.5,-39.2,-41.4,-240.0,-240.0,-240.0,-240.0]},{"orbit":"04","label":"acid","group":"bass","activity":[-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-37.8,-29.0,-32.5,-33.6,-32.8,-32.2,-29.3,-28.1,-28.0,-29.3,-25.5,-22.0,-22.8,-25.7,-32.1,-34.2,-31.0,-31.6,-33.3,-27.9,-29.5,-28.0,-27.5,-32.7,-28.2,-27.7,-29.8,-27.4,-27.9,-32.2,-240.0,-240.0,-240.0,-240.0,-240.0,-30.7,-28.8,-29.0,-27.2,-25.9,-21.0,-26.4,-30.2,-32.4,-30.6,-30.6,-29.8,-44.4,-240.0,-240.0,-240.0,-240.0,-27.9,-29.2,-29.4,-27.6,-26.0,-29.8,-27.0,-27.5,-28.6,-27.4,-27.9,-30.0,-27.2,-28.6,-29.5,-27.3,-26.2,-31.3,-240.0,-240.0,-240.0,-240.0,-42.9,-34.1,-34.5,-35.8,-32.7,-29.7,-34.1,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-36.2,-24.6,-19.0,-18.9,-18.9,-18.3,-19.7,-20.1,-21.0,-22.3,-20.5,-25.6,-18.8,-20.9,-21.1,-22.2,-20.5,-21.7,-27.1,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0]},{"orbit":"05","label":"cpluck","group":"bass","activity":[-19.5,-16.8,-19.5,-19.3,-15.6,-18.1,-21.9,-20.9,-21.6,-21.1,-19.3,-19.9,-17.1,-17.5,-17.9,-17.1,-29.2,-19.8,-19.1,-19.0,-17.0,-15.7,-18.6,-17.5,-12.4,-13.4,-13.0,-13.2,-15.1,-15.5,-15.0,-14.7,-21.6,-16.7,-16.9,-15.2,-16.9,-17.6,-16.3,-19.3,-16.8,-15.0,-16.1,-18.3,-34.8,-30.5,-28.2,-28.0,-28.2,-33.6,-20.1,-16.8,-16.5,-17.1,-16.9,-14.5,-18.1,-17.5,-17.9,-19.2,-19.2,-20.2,-20.8,-20.3,-21.9,-19.8,-26.4,-21.2,-21.4,-19.1,-21.0,-22.8,-24.3,-21.3,-22.6,-23.9,-20.9,-19.2,-19.7,-20.4,-22.7,-30.1,-29.8,-28.2,-24.2,-18.2,-21.2,-19.1,-19.0,-16.6,-18.7,-17.3,-17.3,-19.3,-70.6,-20.5,-20.4,-20.7,-21.8,-20.8,-17.7,-21.1,-21.4,-18.4,-19.5,-19.3,-20.1,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-55.8,-22.4,-19.0,-17.3,-17.1,-18.5,-18.5,-17.4,-20.7,-21.0,-19.0,-21.5,-21.1,-20.5,-21.0,-21.3,-31.5]},{"orbit":"08","label":"break","group":"tops","activity":[-36.0,-34.8,-35.7,-35.3,-32.3,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-58.9,-66.3,-240.0,-240.0,-240.0,-240.0,-37.0,-34.8,-36.2,-46.5,-240.0,-54.2,-34.5,-36.4,-37.1,-240.0,-240.0,-36.8,-36.6,-37.0,-37.7,-34.1,-29.7,-28.5,-23.1,-23.7,-44.2,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-44.3,-30.2,-26.2,-26.0,-76.7,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-39.8,-26.2,-24.3,-21.7,-20.9,-24.6,-22.4,-25.6,-32.6,-26.5,-25.7,-28.3,-25.0,-32.9,-32.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-30.7,-27.5,-29.6,-32.1,-34.3,-35.7,-33.5,-35.3,-37.4,-35.2,-34.6,-34.0,-35.8,-36.8,-36.3,-34.6,-34.3,-35.5,-33.5,-35.7,-33.7,-35.1,-34.8,-34.0,-44.6,-37.1,-35.6,-34.4,-34.3,-35.7,-33.5,-35.4,-32.2,-32.7,-31.8,-35.1,-240.0,-240.0,-240.0,-35.8,-32.2,-42.5,-33.7,-33.0,-30.9,-32.4,-31.9,-31.4,-46.5,-240.0,-240.0,-240.0]},{"orbit":"09","label":"moog sub","group":"bass","activity":[-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-240.0,-34.7,-19.2,-17.5,-17.0,-17.0,-16.3,-17.4,-16.7,-16.3,-17.5,-17.3,-16.8,-17.3,-18.3,-6.3,-3.3,-2.7,-2.9,-3.3,-2.6,-9.0,-240.0,-240.0,-240.0]}]}]}
\ No newline at end of file
import { chromium } from 'playwright'
const url = 'http://localhost:5180/'
const b = await chromium.launch()
// desktop
let p = await b.newPage({ viewport: { width: 1280, height: 900 }, deviceScaleFactor: 2 })
await p.goto(url, { waitUntil: 'networkidle' }).catch(()=>{})
await p.waitForTimeout(5000)
// seek both waveforms to ~55% to light the orbit rails
for (const w of await p.$$('div[tabindex="0"] > div > div:last-child')) {
const box = await w.boundingBox(); if (box) await p.mouse.click(box.x + box.width*0.55, box.y + box.height/2)
}
await p.waitForTimeout(800)
await p.screenshot({ path: '/tmp/armada-desktop.png', fullPage: true })
// mobile
let m = await b.newPage({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 2 })
await m.goto(url, { waitUntil: 'networkidle' }).catch(()=>{})
await m.waitForTimeout(5000)
await m.screenshot({ path: '/tmp/armada-mobile.png', fullPage: true })
await b.close()
console.log('shots done')
import { useEffect, useMemo, useState } from 'react'
import { Download, Anchor } from 'lucide-react'
import type { Note, PlayerData, Variant } from '@/types'
import { TakePanel } from '@/components/TakePanel'
const NOTES_KEY = 'pac_judge_notes'
function useNotes() {
const [notes, setNotes] = useState<Note[]>(() => {
try {
return JSON.parse(localStorage.getItem(NOTES_KEY) || '[]')
} catch {
return []
}
})
useEffect(() => {
localStorage.setItem(NOTES_KEY, JSON.stringify(notes))
}, [notes])
const add = (takeId: string, t: number, text: string) =>
setNotes((ns) =>
[...ns, { id: `${takeId}-${Math.round(t * 1000)}-${ns.length}`, takeId, t, text }].sort(
(a, b) => a.t - b.t,
),
)
return { notes, add }
}
export default function App() {
const [data, setData] = useState<PlayerData | null>(null)
const [variant, setVariant] = useState<Variant>('stream')
const { notes, add } = useNotes()
useEffect(() => {
fetch('/punkachien.json')
.then((r) => r.json())
.then(setData)
.catch(() => setData(null))
}, [])
const exportNotes = () => {
const blob = new Blob([JSON.stringify(notes, null, 2)], { type: 'application/json' })
const a = document.createElement('a')
a.href = URL.createObjectURL(blob)
a.download = 'punkachien_judge_notes.json'
a.click()
URL.revokeObjectURL(a.href)
}
const matchNote = useMemo(
() =>
variant === 'stream'
? 'A/B is loudness-matched — both takes at the −14 LUFS streaming target.'
: 'A/B is loudness-matched — both takes at the club target (≈ −11.4 LUFS; true −9 needs a limiter stage).',
[variant],
)
if (!data) {
return (
<div className="grid min-h-svh place-items-center text-ink-muted">
<p className="animate-pulse text-sm">loading the bridge…</p>
</div>
)
}
return (
<div className="mx-auto flex min-h-svh max-w-6xl flex-col gap-6 px-4 py-5">
<header className="flex flex-wrap items-end justify-between gap-3 border-b border-hairline pb-4">
<div>
<p className="flex items-center gap-1.5 text-[11px] uppercase tracking-widest text-ink-faint">
<Anchor size={12} /> L&apos;Armada · judge
</p>
<h1 className="text-2xl font-semibold tracking-tight">
{data.track} <span className="text-ink-muted">— A/B</span>
</h1>
<p className="mt-0.5 text-xs text-ink-faint">{data.calibration}</p>
</div>
<div className="flex items-center gap-3">
<div className="flex rounded-md border border-hairline p-0.5 text-xs">
{(['stream', 'club'] as Variant[]).map((v) => (
<button
key={v}
onClick={() => setVariant(v)}
className={`rounded px-3 py-1 capitalize transition-colors ${
variant === v ? 'bg-ink text-surface' : 'text-ink-muted hover:text-ink'
}`}
>
{v}
</button>
))}
</div>
<button
onClick={exportNotes}
disabled={!notes.length}
className="flex items-center gap-1.5 rounded-md bg-raised px-3 py-1.5 text-xs
font-medium hover:bg-overlay disabled:opacity-30"
>
<Download size={13} /> Export notes
</button>
</div>
</header>
<p className="-mt-2 text-xs text-ink-muted">{matchNote}</p>
<div className="grid flex-1 gap-4 lg:grid-cols-2">
{data.takes.map((take) => (
<TakePanel
key={take.id}
take={take}
variant={variant}
roleGroups={data.roleGroups}
activeDb={data.activeDb}
calibration={data.calibration}
notes={notes.filter((n) => n.takeId === take.id)}
onAddNote={(t, text) => add(take.id, t, text)}
/>
))}
</div>
<footer className="border-t border-hairline pt-3 text-[11px] text-ink-faint">
Independent transports — each take is its own performance. Space toggles the focused
player. Notes persist locally; export feeds the ear-archive.
</footer>
</div>
)
}
<svg xmlns="http://www.w3.org/2000/svg" width="77" height="47" fill="none" aria-labelledby="vite-logo-title" viewBox="0 0 77 47"><title id="vite-logo-title">Vite</title><style>.parenthesis{fill:#000}@media (prefers-color-scheme:dark){.parenthesis{fill:#fff}}</style><path fill="#9135ff" d="M40.151 45.71c-.663.844-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.493c-.92 0-1.457-1.04-.92-1.788l7.479-10.471c1.07-1.498 0-3.578-1.842-3.578H15.443c-.92 0-1.456-1.04-.92-1.788l9.696-13.576c.213-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.472c-1.07 1.497 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.087.89 1.83L40.153 45.712z"/><mask id="a" width="48" height="47" x="14" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M40.047 45.71c-.663.843-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.389c-.92 0-1.457-1.04-.92-1.788l7.479-10.472c1.07-1.497 0-3.578-1.842-3.578H15.34c-.92 0-1.456-1.04-.92-1.788l9.696-13.575c.213-.297.556-.474.92-.474H53.93c.92 0 1.456 1.04.92 1.788L47.37 13.03c-1.07 1.498 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.088.89 1.831L40.049 45.712z"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#eee6ff" rx="5.508" ry="14.704" transform="rotate(269.814 20.96 11.29)scale(-1 1)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#eee6ff" rx="10.399" ry="29.851" transform="rotate(89.814 -16.902 -8.275)scale(1 -1)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#8900ff" rx="5.508" ry="30.487" transform="rotate(89.814 -19.197 -7.127)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.928 4.177)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.738 5.52)scale(1 -1)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#eee6ff" rx="14.072" ry="22.078" transform="rotate(93.35 31.245 55.578)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx="14.592" cy="9.743" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(39.51 14.592 9.743)"/></g><g filter="url(#k)"><ellipse cx="61.728" cy="-5.321" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 61.728 -5.32)"/></g><g filter="url(#l)"><ellipse cx="55.618" cy="7.104" fill="#00c2ff" rx="5.971" ry="9.665" transform="rotate(37.892 55.618 7.104)"/></g><g filter="url(#m)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#n)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#o)"><ellipse cx="49.857" cy="30.678" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 49.857 30.678)"/></g><g filter="url(#p)"><ellipse cx="52.623" cy="33.171" fill="#00c2ff" rx="5.971" ry="15.297" transform="rotate(37.892 52.623 33.17)"/></g></g><path d="M6.919 0c-9.198 13.166-9.252 33.575 0 46.789h6.215c-9.25-13.214-9.196-33.623 0-46.789zm62.424 0h-6.215c9.198 13.166 9.252 33.575 0 46.789h6.215c9.25-13.214 9.196-33.623 0-46.789" class="parenthesis"/><defs><filter id="b" width="60.045" height="41.654" x="-5.564" y="16.92" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-40.407" y="-6.762" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-35.435" y="2.801" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-30.84" y="20.8" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-29.307" y="21.949" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="29.961" y="-17.13" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="37.754" y="3.055" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="37.754" y="3.055" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-13.43" y="-22.082" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="34.321" y="-37.644" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="38.847" y="-10.552" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-15.081" y="6.78" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-15.081" y="6.78" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="22.45" y="-1.645" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="32.919" y="11.36" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter></defs></svg>
import type { LoudTrace, MasterInfo } from '@/types'
import { fmtDb, loudFrac } from '@/lib/format'
interface Props {
master: MasterInfo
loud: LoudTrace
currentTime: number
}
function Stat({ label, value, warn }: { label: string; value: string; warn?: boolean }) {
return (
<div className="flex flex-col">
<span className="text-[10px] uppercase tracking-wide text-ink-faint">{label}</span>
<span className={`font-mono text-sm tnum ${warn ? 'text-blocked' : 'text-ink'}`}>{value}</span>
</div>
)
}
/** Loudness instrument: live momentary-LUFS bar (with integrated target mark) + I/LRA/TP. */
export function Meter({ master, loud, currentTime }: Props) {
const i = Math.min(loud.trace.length - 1, Math.max(0, Math.floor(currentTime / loud.stepS)))
const m = loud.trace[i] ?? -70
const frac = loudFrac(m)
const targetFrac = master.I != null ? loudFrac(master.I) : null
const tpWarn = master.TP != null && master.TP > -1
return (
<div className="flex items-center gap-4">
{/* momentary meter */}
<div className="min-w-0 flex-1">
<div className="mb-1 flex items-baseline justify-between">
<span className="text-[10px] uppercase tracking-wide text-ink-faint">Momentary</span>
<span className="font-mono text-xs text-ink-muted tnum">{fmtDb(m, ' LUFS')}</span>
</div>
<div className="relative h-2 overflow-hidden rounded-full bg-overlay">
<div
className="h-full rounded-full bg-ink transition-[width] duration-75"
style={{ width: `${frac * 100}%` }}
/>
{targetFrac != null && (
<div
className="absolute inset-y-0 w-px bg-magenta"
style={{ left: `${targetFrac * 100}%` }}
title={`integrated ${fmtDb(master.I)} LUFS`}
/>
)}
</div>
</div>
<div className="flex shrink-0 gap-4">
<Stat label="Integrated" value={fmtDb(master.I, ' LUFS')} />
<Stat label="LRA" value={fmtDb(master.LRA, ' LU')} />
<Stat label="True pk" value={fmtDb(master.TP, ' dB')} warn={tpWarn} />
</div>
</div>
)
}
import { useMemo } from 'react'
import type { RoleGroup, Take } from '@/types'
import { loudFrac } from '@/lib/format'
interface Props {
take: Take
roleGroups: RoleGroup[]
currentTime: number
activeDb: number
}
/**
* The orbit-group rail: five labelled lanes (role families), showing the orbits
* active in THIS take, lit by their RMS at the playhead. Per-track, grouped by
* measured register. Color reinforces label + glyph + lane (never hue alone).
*/
export function OrbitRail({ take, roleGroups, currentTime, activeDb }: Props) {
const bin = Math.max(0, Math.floor(currentTime / take.binS))
const byGroup = useMemo(() => {
const m: Record<string, Take['orbits']> = {}
for (const o of take.orbits) (m[o.group] ??= []).push(o)
return m
}, [take])
return (
<div className="flex flex-col gap-px overflow-hidden rounded-md border border-hairline">
{roleGroups.map((g) => {
const orbits = byGroup[g.key] ?? []
const anyLive = orbits.some((o) => (o.activity[bin] ?? -240) > activeDb)
return (
<div key={g.key} className="flex items-stretch gap-2 bg-raised px-2 py-1.5">
<div
className="flex w-20 shrink-0 items-center gap-1.5 text-xs font-medium"
style={{ color: anyLive ? g.color : 'var(--color-ink-faint)' }}
>
<span aria-hidden className="text-sm leading-none">{g.glyph}</span>
<span>{g.label}</span>
</div>
<div className="flex min-w-0 flex-wrap items-center gap-1.5">
{orbits.length === 0 && (
<span className="text-xs text-ink-faint/60"></span>
)}
{orbits.map((o) => {
const db = o.activity[bin] ?? -240
const live = db > activeDb
const frac = loudFrac(db)
return (
<span
key={o.orbit}
title={`orbit-${o.orbit} · ${o.label} · ${isFinite(db) ? db.toFixed(0) : '−∞'} dB`}
className="relative flex items-center gap-1 overflow-hidden rounded-full
border px-2 py-0.5 font-mono text-[11px] tnum transition-colors"
style={{
borderColor: live ? g.color : 'var(--color-hairline)',
color: live ? g.color : 'var(--color-ink-faint)',
background: live ? `${g.color}1a` : 'transparent',
}}
>
{/* level fill — reinforces loudness without relying on hue alone */}
{live && (
<span
aria-hidden
className="absolute inset-y-0 left-0"
style={{ width: `${frac * 100}%`, background: `${g.color}14` }}
/>
)}
<span className="relative">{o.label}</span>
<span className="relative opacity-60">·{o.orbit}</span>
</span>
)
})}
</div>
</div>
)
})}
</div>
)
}
import { useState } from 'react'
import { MapPin, CheckCircle2 } from 'lucide-react'
import type { Note, RoleGroup, Take, Variant } from '@/types'
import { WaveformPlayer } from './WaveformPlayer'
import { Meter } from './Meter'
import { OrbitRail } from './OrbitRail'
import { mmss } from '@/lib/format'
interface Props {
take: Take
variant: Variant
roleGroups: RoleGroup[]
activeDb: number
calibration: string
notes: Note[]
onAddNote: (t: number, text: string) => void
}
export function TakePanel({ take, variant, roleGroups, activeDb, calibration, notes, onAddNote }: Props) {
const [t, setT] = useState(0)
const [draft, setDraft] = useState('')
const master = take.masters[variant]
const submit = () => {
const text = draft.trim()
if (!text) return
onAddNote(t, text)
setDraft('')
}
return (
<section className="flex flex-col gap-4 rounded-lg border border-hairline bg-raised/40 p-4">
<header className="flex items-start justify-between gap-3">
<div className="min-w-0">
<h2 className="text-lg font-semibold tracking-tight">{take.label}</h2>
<p className="flex items-center gap-1 text-xs text-ink-muted">
<MapPin size={12} /> {take.gig} · {take.date}
</p>
</div>
<span
title={calibration}
className="flex shrink-0 items-center gap-1 rounded-sm border border-hairline
px-1.5 py-0.5 text-[10px] text-ink-faint"
>
<CheckCircle2 size={11} className="text-ready" /> calibrated
</span>
</header>
<WaveformPlayer url={`/audio/${master.file}`} dur={take.dur} onTime={setT} />
<Meter master={master} loud={take.loud[variant]} currentTime={t} />
<OrbitRail take={take} roleGroups={roleGroups} currentTime={t} activeDb={activeDb} />
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<span className="font-mono text-xs text-ink-faint tnum">@ {mmss(t)}</span>
<input
value={draft}
onChange={(e) => setDraft(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && submit()}
placeholder="note this moment…"
className="min-w-0 flex-1 rounded-md border border-hairline bg-surface px-2 py-1
text-sm outline-none placeholder:text-ink-faint
focus-visible:ring-2 focus-visible:ring-magenta/60"
/>
<button
onClick={submit}
className="rounded-md bg-raised px-3 py-1 text-xs font-medium text-ink
hover:bg-overlay disabled:opacity-30"
disabled={!draft.trim()}
>
Mark
</button>
</div>
{notes.length > 0 && (
<ul className="flex flex-col gap-1">
{notes.map((n) => (
<li key={n.id} className="flex gap-2 text-xs">
<span className="font-mono text-ink-faint tnum">{mmss(n.t)}</span>
<span className="text-ink-muted">{n.text}</span>
</li>
))}
</ul>
)}
</div>
</section>
)
}
import { useEffect, useRef, useState } from 'react'
import WaveSurfer from 'wavesurfer.js'
import { Play, Pause } from 'lucide-react'
import { mmss } from '@/lib/format'
interface Props {
url: string
dur: number
/** called with current playback time (s) on every frame + on seek */
onTime: (t: number) => void
}
/** A single take's transport: wavesurfer waveform, click-seek, Space=play/pause. */
export function WaveformPlayer({ url, dur, onTime }: Props) {
const elRef = useRef<HTMLDivElement>(null)
const wsRef = useRef<WaveSurfer | null>(null)
const onTimeRef = useRef(onTime)
onTimeRef.current = onTime
const [playing, setPlaying] = useState(false)
const [ready, setReady] = useState(false)
const [t, setT] = useState(0)
useEffect(() => {
if (!elRef.current) return
const ws = WaveSurfer.create({
container: elRef.current,
height: 76,
waveColor: '#6a6a70', // ink-faint
progressColor: '#e8e8ea', // ink
cursorColor: '#d900ff', // brand magenta — the now
cursorWidth: 2,
barWidth: 2,
barGap: 1,
barRadius: 1,
normalize: true,
url,
})
wsRef.current = ws
setReady(false)
setPlaying(false)
const emit = (time: number) => {
setT(time)
onTimeRef.current(time)
}
ws.on('ready', () => setReady(true))
ws.on('timeupdate', emit)
ws.on('interaction', () => emit(ws.getCurrentTime()))
ws.on('play', () => setPlaying(true))
ws.on('pause', () => setPlaying(false))
ws.on('finish', () => setPlaying(false))
return () => {
ws.destroy()
}
}, [url])
const toggle = () => wsRef.current?.playPause()
return (
<div
tabIndex={0}
onKeyDown={(e) => {
if (e.code === 'Space') {
e.preventDefault()
toggle()
}
}}
className="group rounded-md outline-none focus-visible:ring-2 focus-visible:ring-magenta/60"
>
<div className="flex items-center gap-3">
<button
onClick={toggle}
disabled={!ready}
aria-label={playing ? 'Pause' : 'Play'}
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-md
bg-ink text-surface transition-opacity hover:opacity-90
disabled:opacity-30"
>
{playing ? <Pause size={18} fill="currentColor" /> : <Play size={18} fill="currentColor" />}
</button>
<div ref={elRef} className="min-w-0 flex-1" />
</div>
<div className="mt-1 flex justify-between font-mono text-xs text-ink-muted tnum">
<span>{ready ? mmss(t) : 'loading…'}</span>
<span>{mmss(dur)}</span>
</div>
</div>
)
}
declare module '@fontsource-variable/geist'
declare module '@fontsource-variable/geist-mono'
@import "tailwindcss";
/* ── L'Armada design tokens — "The Ship's Bridge" (see armada/DESIGN.md) ── */
@theme {
--color-surface: #0a0a0a;
--color-raised: #111111;
--color-overlay: #171717;
--color-hairline: #ffffff1f;
--color-ink: #e8e8ea;
--color-ink-muted: #9a9aa0;
--color-ink-faint: #6a6a70;
/* brand — reserved (One Voice Rule, ≤10%): the now + brand + one primary action */
--color-magenta: #d900ff;
/* role families (orbits, measured register) — always paired with label + glyph */
--color-percs: #ff8c00;
--color-bass: #7c5cff;
--color-melodic: #36c5f0;
--color-tops: #2dd4bf;
--color-atmos: #8a93a6;
--color-vox: #ff3d7b;
/* functional */
--color-blocked: #ff5252;
--color-ready: #5bc091;
--font-sans: "Geist Variable", Inter, system-ui, sans-serif;
--font-mono: "Geist Mono Variable", ui-monospace, SFMono-Regular, monospace;
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 10px;
}
@layer base {
html { color-scheme: dark; }
body {
margin: 0;
background: var(--color-surface);
color: var(--color-ink);
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
/* instrument readouts: tabular so digits don't jitter as they update */
.tnum { font-variant-numeric: tabular-nums; }
}
@media (prefers-reduced-motion: reduce) {
* { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
}
/** seconds → m:ss.d */
export function mmss(t: number, decimals = 1): string {
if (!isFinite(t)) return '0:00'
const m = Math.floor(t / 60)
const s = t - m * 60
return `${m}:${s.toFixed(decimals).padStart(decimals ? 4 + decimals : 2, '0')}`
}
/** dB value → 0..1 across a loudness window (default -40..-3 LUFS) */
export function loudFrac(db: number, lo = -40, hi = -3): number {
if (!isFinite(db)) return 0
return Math.max(0, Math.min(1, (db - lo) / (hi - lo)))
}
export function fmtDb(v: number | null, unit = ''): string {
return v == null ? '—' : `${v > 0 ? '+' : ''}${v.toFixed(1)}${unit}`
}
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import '@fontsource-variable/geist'
import '@fontsource-variable/geist-mono'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
// Shape of public/punkachien.json (produced by build_player_data.py).
export interface RoleGroup {
key: string
label: string
color: string
glyph: string
}
export interface Orbit {
orbit: string
label: string
group: string
/** RMS dB per `binS` bin, aligned to track t0 */
activity: number[]
}
export interface MasterInfo {
file: string
I: number | null // integrated LUFS
LRA: number | null
TP: number | null // true peak dBFS
}
export interface LoudTrace {
/** momentary LUFS per `stepS` */
trace: number[]
stepS: number
}
export type Variant = 'stream' | 'club'
export interface Take {
id: string
label: string
gig: string
date: string
dur: number
binS: number
masters: Record<Variant, MasterInfo>
loud: Record<Variant, LoudTrace>
orbits: Orbit[]
}
export interface PlayerData {
track: string
calibration: string
activeDb: number
roleGroups: RoleGroup[]
takes: Take[]
}
export interface Note {
id: string
takeId: string
t: number // seconds
text: string
}
{
"compilerOptions": {
"paths": { "@/*": ["./src/*"] },
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM"],
"module": "esnext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"module": "esnext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}
import { defineConfig, type Plugin } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import path from 'node:path'
import fs from 'node:fs'
const AUDIO_DIR = path.resolve(__dirname, '../tide-table/punkachien')
/**
* Dev-only: serve /audio/* from the punkachien dir with HTTP Range support,
* so wavesurfer can stream + seek the big FLACs without copying them into the
* bundle. In production, `serve.py --dir dist` with a `dist/audio` symlink does
* the same job (created post-build), keeping /audio a stable base everywhere.
*/
function audioDevServer(): Plugin {
return {
name: 'armada-audio-dev',
apply: 'serve',
configureServer(server) {
server.middlewares.use('/audio', (req, res, next) => {
const rel = decodeURIComponent((req.url || '').split('?')[0])
const file = path.join(AUDIO_DIR, rel)
if (!file.startsWith(AUDIO_DIR) || !fs.existsSync(file) || !fs.statSync(file).isFile())
return next()
const size = fs.statSync(file).size
const range = req.headers.range
res.setHeader('Accept-Ranges', 'bytes')
res.setHeader('Content-Type', 'audio/flac')
if (range) {
const m = /bytes=(\d*)-(\d*)/.exec(range)
const start = m && m[1] ? parseInt(m[1], 10) : 0
const end = m && m[2] ? parseInt(m[2], 10) : size - 1
res.statusCode = 206
res.setHeader('Content-Range', `bytes ${start}-${end}/${size}`)
res.setHeader('Content-Length', String(end - start + 1))
fs.createReadStream(file, { start, end }).pipe(res)
} else {
res.setHeader('Content-Length', String(size))
fs.createReadStream(file).pipe(res)
}
})
},
}
}
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss(), audioDevServer()],
resolve: { alias: { '@': path.resolve(__dirname, './src') } },
server: { host: true }, // expose on LAN for phone audition (same wifi)
})
#!/usr/bin/env python3
"""Sample TF-IDF across the .tidal corpus — derive which samples *identify* a track.
The idea (PLN): treat each `.tidal` score as a document and each Dirt-Samples
folder name as a term. Then:
- **DF / IDF** tells us how discriminative a sample is. `jungle_breaks` is
everywhere (low IDF, useless for ID); `cpluck` is rare (high IDF, a *tell*).
- **TF-IDF per track** ranks each track's signature samples.
This is the principled replacement for hand-picking fingerprint features, and the
weighting layer for locate-matrix L3 (orbit/sample match weighted by IDF, so common
percs don't dominate). Pure text — reads the local git corpus only, never freebox.
Vocabulary = folder names under the *local* Dirt-Samples (custom packs are symlinked
in, e.g. jungle_breaks → MethLab/…). Membership in that set separates samples from
synths (moog, supersaw), control params (gain, room, n), and mininotation noise.
Usage:
sample_tfidf.py # report + write JSON
sample_tfidf.py --track punkachien # focus one track's signature
sample_tfidf.py --sample cpluck # where is this sample used + how rare
"""
import argparse, math, re, sys
from collections import Counter
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "armada" / "tide-table"))
from models import TfidfReport # noqa: E402
CORPUS = Path("/home/pln/Work/Sound/Tidal")
DIRT = Path("/home/pln/.local/share/SuperCollider/downloaded-quarks/Dirt-Samples")
OUT = CORPUS / "armada" / "tide-table" / "sample_tfidf.json"
# split a quoted mininotation string into candidate tokens. KEEP '_' and digits
# (vec1_acid, jungle_breaks, breaks165); split on whitespace + mininotation ops;
# ':' splits off the sample index (cpluck:1 -> cpluck).
SPLIT = re.compile(r"[\s\[\](){}<>*/.~?!@|,;:%+\-]+")
# Sound-CONTEXT only: a string is a sample pattern iff it's the arg of `s`/`sound`
# or the `# "..."` sound-shorthand. This excludes mask/struct booleans ("t f"),
# note patterns (n/note "..."), MIDI ctrl ("^49"), range args, etc. — which would
# otherwise collide with Dirt folder names ("f", "d", "e") and pollute the signal.
SOUND_CTX = re.compile(r'(?:\bsound\b|\bs\b|#)\s*"([^"]*)"')
def vocab():
"""Authoritative sample names = entries under local Dirt-Samples."""
return {p.name for p in DIRT.iterdir() if not p.name.startswith(".")}
def samples_in(text, vocab):
"""Multiset of sample tokens present in one .tidal, sound-context only."""
counts = Counter()
for q in SOUND_CTX.findall(text):
for tok in SPLIT.split(q):
if len(tok) > 1 and tok in vocab: # drop 1-char note/bool collisions
counts[tok] += 1
return counts
def build():
voc = vocab()
files = sorted(CORPUS.rglob("*.tidal"))
docs = {} # rel_path -> Counter(sample -> tf)
df = Counter() # sample -> # docs containing it
for f in files:
try:
text = f.read_text(errors="ignore")
except Exception:
continue
c = samples_in(text, voc)
if not c:
continue
rel = str(f.relative_to(CORPUS))
docs[rel] = c
for s in c:
df[s] += 1
n = len(docs)
# smoothed idf
idf = {s: round(math.log((n + 1) / (d + 1)) + 1, 3) for s, d in df.items()}
tracks = {}
for rel, c in docs.items():
tfidf = {s: round(tf * idf[s], 3) for s, tf in c.items()}
top = sorted(tfidf.items(), key=lambda kv: -kv[1])[:6]
tracks[rel] = {"n_samples": len(c), "tf": dict(c),
"top_tfidf": [{"sample": s, "score": v, "df": df[s]}
for s, v in top]}
return {
"corpus": str(CORPUS), "n_docs": n, "vocab_size": len(voc),
"df": dict(df.most_common()),
"idf": dict(sorted(idf.items(), key=lambda kv: -kv[1])),
"tracks": tracks,
}
def report(data, args):
n = data["n_docs"]
df = data["df"]
idf = data["idf"]
if args.sample:
s = args.sample
if s not in df:
print(f"'{s}' not used in any .tidal (or not a Dirt-Samples name).")
return
users = [(rel, t["tf"][s]) for rel, t in data["tracks"].items() if s in t["tf"]]
users.sort(key=lambda x: -x[1])
print(f"\n■ '{s}' df={df[s]}/{n} docs idf={idf[s]} "
f"({'RARE tell' if df[s] <= 3 else 'common' if df[s] >= 20 else 'mid'})")
print(f" used in {len(users)} tracks:")
for rel, tf in users[:25]:
print(f" {tf:>3}× {rel}")
return
if args.track:
hits = {rel: t for rel, t in data["tracks"].items() if args.track in rel}
for rel, t in hits.items():
print(f"\n■ {rel} ({t['n_samples']} distinct samples)")
print(" signature (TF-IDF):")
for h in t["top_tfidf"]:
d = df[h["sample"]]
print(f" {h['score']:>7} {h['sample']:<20} (df={d}, "
f"{'rare' if d <= 3 else 'common' if d >= 20 else 'mid'})")
if not hits:
print(f"no track matching '{args.track}'")
return
print(f"\nCorpus: {n} .tidal docs · vocab {data['vocab_size']} sample names\n")
rare = [s for s, d in df.items() if d == 1]
print(f"■ RARE TELLS (df=1, used in exactly one track) — {len(rare)} samples")
for s in list(df)[::-1][:25]:
if df[s] <= 2:
print(f" df={df[s]} idf={idf[s]:<6} {s}")
print(f"\n■ COMMON / ubiquitous (high df, weak for ID):")
for s, d in list(df.items())[:18]:
print(f" df={d:>4} idf={idf[s]:<6} {s}")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--track", help="focus one track's signature samples")
ap.add_argument("--sample", help="where is this sample used + how rare")
ap.add_argument("--no-write", action="store_true")
args = ap.parse_args()
data = build()
report(data, args)
if not args.no_write:
rep = TfidfReport.model_validate(data) # validate schema on write
OUT.write_text(rep.model_dump_json(indent=1))
print(f"\n✓ {OUT} ({OUT.stat().st_size/1024:.0f} KB) [validated TfidfReport]")
if __name__ == "__main__":
main()
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