Lane E R13 (v3.0-beta): gigKey rename + grunge/covers beds + instrument decimation
- debt #1: the manifest music key IS the gigKey, always gig-<genreKey>. Renamed pubrock-live -> gig-pubrock (files + gen_audio + build_manifest); old files deleted. B/F switch readers, F deletes the GIG_BED bridge (both fail soft to silence til then). - two new gig beds: gig-grunge (band_room, quiet-loud dynamic) + gig-covers (RSL, mellow organ-led); procedural/seeded/loopable/$0. New organ() + grungechord() synth helpers. - decimate the 5 instrument GLBs to 14k tris each (was 43-96k; fleet 236k->70k, -70%) via collapse (thin features survive); _hi hi-poly originals kept for depot; thumbs re-rendered. - manifest regen (validates 0/0 incl --depot); app boots clean flags-off / ?gigs=1 (3-venue district, all 3 beds resolve) / ?noassets=1&gigs=1 (0 audio+GLB fetches). - depot re-publish (14k bytes + _hi originals) gated on John's OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ -1,5 +1,71 @@
|
||||
# LANE E — cross-lane notes
|
||||
|
||||
## Round 13 — two genres, the gigKey rename (debt #1), lighter instruments
|
||||
|
||||
**v3.0-beta THE DISTRICT.** All local work landed + verified; the one gated step (depot publish)
|
||||
waits on John. Verification: core gates GREEN (scaffold · consistency 0-warn · citygen selfcheck
|
||||
3273/3273 incl. v3 gig-layer + assets-on-disk), manifest validator `--depot` GREEN, and the real
|
||||
app boots clean flags-off / `?gigs=1` (A's 3-venue district, 14 gigs/posters — all three gig beds
|
||||
resolve by canonical key, no stale key, 0 console errors) / `?noassets=1&gigs=1` (0 audio + 0 GLB
|
||||
fetches, silent-and-fine).
|
||||
|
||||
### 1. gigKey rename — DONE (debt #1). **→ Lane B + Lane F: switch your readers.**
|
||||
The manifest music key IS the gigKey, **always `gig-<genreKey>`** — one key, zero mapping. Renamed
|
||||
`pubrock-live` → **`gig-pubrock`**; added `gig-grunge`, `gig-covers`. Files are now
|
||||
`audio/music-gig-{pubrock,grunge,covers}.{ogg,m4a}`; the old `music-pubrock-live.*` are **deleted**.
|
||||
Manifest regen'd + validates 0/0; no stale `pubrock-live` anywhere.
|
||||
- **→ Lane F (interior_mode.js):** DELETE the `GIG_BED` bridge (the `{'gig-pubrock':'pubrock-live'}`
|
||||
map + `gigBedKey()`); read `ra.gigKey` (`gig-<genre>`) straight against `AUDIO.music[...]`. The
|
||||
bridge currently maps to the now-gone `pubrock-live` → **fails soft to silence** (I verified: no
|
||||
crash, just a silent band) until you delete it. This is your R13 task #4.
|
||||
- **→ Lane B (audio.js ~L274/276):** you hardcode `'pubrock-live'` for the street spill — read each
|
||||
venue's `gigKey` bed instead (`AUDIO.music[venue.gigKey]`, i.e. `'gig-'+genreKey`). Same
|
||||
fail-soft-silent until you switch. Your R13 task #2 (per-genre spill) covers this.
|
||||
- **CITY_SPEC §v3 contract line (coordinate wording w/ A):** *"The audio manifest key for a gig
|
||||
genre IS the gigKey, always `gig-<genreKey>` (`gig-pubrock`, `gig-grunge`, `gig-covers`). No
|
||||
mapping table: C emits `room.audio.gigKey`, E names the bed, B/F read the same key."*
|
||||
|
||||
### 2. Two new gig beds — grunge (band_room) + covers (RSL).
|
||||
Procedural, seeded, loopable, OGG+M4A, loudnorm, $0 on-device (same discipline as R11/12). All three
|
||||
are audibly + spectrally distinct (spectral centroids pubrock 2336 / grunge 2271 / covers 1115 Hz;
|
||||
contact sheet `docs/shots/laneE/gig_beds_spectrograms.png`):
|
||||
- **`gig-grunge`** (0.6 gain, 17s): down-tuned sludge, quiet-loud dynamic (clean-ish verse → full-fuzz
|
||||
chorus — visible in the spectrogram), loose heavy kit. Sits at −14.6 LUFS (genre-dense; a 1.02
|
||||
*inter-sample* peak, not real clipping — plays at gain 0.6 so absolute output ≈0.6).
|
||||
- **`gig-covers`** (0.5 gain, 19s): mellow RSL covers band — new drawbar-`organ()` voice, walking
|
||||
bass, soft kit, sparse organ answer. Warm, dark highs (that low continuous band + horizontal
|
||||
harmonic striations in the spectrogram = the organ). New synth helpers: `organ()`, `grungechord()`.
|
||||
- **John may re-flavour genres (names only) — I synthesize to order.** Note it's now a coordinated
|
||||
A+E change: the genreKey lives in A's `registry.js` (drives `gigKeyFor` → the bed name), so a
|
||||
re-flavour = A renames the genreKey + E renames/re-synths the bed. Defaults grunge/covers are live.
|
||||
|
||||
### 3. Instrument decimation — DONE. 5 GLBs → 14k tris each (≤15k target). **→ Lane D.**
|
||||
Collapse-decimated (Blender, `pipeline/decimate_props.py`) — **not** voxel-remesh, so thin features
|
||||
survive: mic pole + hanging cable, drum cymbals/stands, guitar necks/headstocks all read (thumbs
|
||||
re-rendered + eyeballed). Dims/orientation/base-origin/WebP preserved; GLB-law clean (no Draco, tex
|
||||
≤1024). Fleet **236k → 70k tris (−70%)** across the district's multiple stages.
|
||||
|
||||
| instrument | tris (norm→14k) | footprint (m) | height |
|
||||
|---|---|---|---|
|
||||
| electric_guitar | 43.5k → 14k | 0.38×0.08 | 1.00 |
|
||||
| bass_guitar | 36.0k → 14k | 0.34×0.13 | 1.15 |
|
||||
| mic_stand | 34.1k → 14k | 0.57×0.57 | 1.70 |
|
||||
| drum_kit | 26.2k → 14k | 1.50×1.24 | 1.30 |
|
||||
| guitar_amp | 96.6k → 14k | 0.52×0.29 | 0.61 |
|
||||
|
||||
- **→ Lane D:** wire against the SAME filenames (`procity_fit_{electric_guitar,bass_guitar,mic_stand,
|
||||
drum_kit,guitar_amp}_01.glb`) — footprints/heights in the manifest are **unchanged**, so your
|
||||
`instrumentFor(role)` fittings are unaffected; they're just lighter now. (debt #2)
|
||||
- High-poly originals preserved as `_hi` variants in `_normalized/` (git-ignored) for the depot.
|
||||
|
||||
### 4. Depot publish — GATED on John (the one outward-facing step). Gate 3 flag.
|
||||
Ground truth: the 5 base instrument **names are LIVE** on the depot, but serving the **stale R12
|
||||
high-poly bytes** (43–96k). My 14k versions + the 5 `_hi` originals are **local-only** until a
|
||||
re-publish. `validate_manifest --depot` is GREEN (names resolve) but can't see the stale bytes — so
|
||||
**the decimation win only reaches production after a push.** On John's OK: `publish.py` overwrites the
|
||||
5 base names with the 14k GLBs + uploads the 5 `_hi` originals (idempotent, reversible). Until then I
|
||||
carry the flag; ?localdepot already serves the 14k so QA/local dev see the light versions.
|
||||
|
||||
## Round 9 — VRAM audit verdict + one rec for C
|
||||
|
||||
**VERDICT: fine to ship v2.0.** Texture VRAM ~240 MB default boot / ~420 MB worst-case (all flags +
|
||||
|
||||
BIN
docs/shots/laneE/gig_beds_spectrograms.png
Normal file
|
After Width: | Height: | Size: 618 KiB |
@ -6,16 +6,17 @@
|
||||
"out": "pipeline/_normalized/procity_fit_electric_guitar_01.glb",
|
||||
"thumb": "web/assets/thumbs/procity_fit_electric_guitar_01.png",
|
||||
"tri_before": 181716,
|
||||
"tri_after": 43484,
|
||||
"tri_after": 14000,
|
||||
"size_m": [
|
||||
0.38,
|
||||
1.0,
|
||||
0.077
|
||||
0.078
|
||||
],
|
||||
"footprint": [
|
||||
0.38,
|
||||
0.08
|
||||
]
|
||||
],
|
||||
"hi_variant": "procity_fit_electric_guitar_01_hi.glb"
|
||||
},
|
||||
{
|
||||
"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/bass-guitar.glb",
|
||||
@ -23,16 +24,17 @@
|
||||
"out": "pipeline/_normalized/procity_fit_bass_guitar_01.glb",
|
||||
"thumb": "web/assets/thumbs/procity_fit_bass_guitar_01.png",
|
||||
"tri_before": 192238,
|
||||
"tri_after": 36048,
|
||||
"tri_after": 14000,
|
||||
"size_m": [
|
||||
0.337,
|
||||
1.15,
|
||||
0.124
|
||||
0.125
|
||||
],
|
||||
"footprint": [
|
||||
0.34,
|
||||
0.12
|
||||
]
|
||||
0.13
|
||||
],
|
||||
"hi_variant": "procity_fit_bass_guitar_01_hi.glb"
|
||||
},
|
||||
{
|
||||
"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/drum-kit.glb",
|
||||
@ -40,16 +42,17 @@
|
||||
"out": "pipeline/_normalized/procity_fit_drum_kit_01.glb",
|
||||
"thumb": "web/assets/thumbs/procity_fit_drum_kit_01.png",
|
||||
"tri_before": 197990,
|
||||
"tri_after": 29890,
|
||||
"tri_after": 14000,
|
||||
"size_m": [
|
||||
1.498,
|
||||
1.497,
|
||||
1.3,
|
||||
1.242
|
||||
],
|
||||
"footprint": [
|
||||
1.5,
|
||||
1.24
|
||||
]
|
||||
],
|
||||
"hi_variant": "procity_fit_drum_kit_01_hi.glb"
|
||||
},
|
||||
{
|
||||
"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/guitar-amp.glb",
|
||||
@ -57,16 +60,17 @@
|
||||
"out": "pipeline/_normalized/procity_fit_guitar_amp_01.glb",
|
||||
"thumb": "web/assets/thumbs/procity_fit_guitar_amp_01.png",
|
||||
"tri_before": 180813,
|
||||
"tri_after": 96592,
|
||||
"tri_after": 14000,
|
||||
"size_m": [
|
||||
0.511,
|
||||
0.6,
|
||||
0.285
|
||||
0.516,
|
||||
0.607,
|
||||
0.294
|
||||
],
|
||||
"footprint": [
|
||||
0.51,
|
||||
0.52,
|
||||
0.29
|
||||
]
|
||||
],
|
||||
"hi_variant": "procity_fit_guitar_amp_01_hi.glb"
|
||||
},
|
||||
{
|
||||
"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/mic-stand.glb",
|
||||
@ -74,16 +78,17 @@
|
||||
"out": "pipeline/_normalized/procity_fit_mic_stand_01.glb",
|
||||
"thumb": "web/assets/thumbs/procity_fit_mic_stand_01.png",
|
||||
"tri_before": 162256,
|
||||
"tri_after": 34124,
|
||||
"tri_after": 14000,
|
||||
"size_m": [
|
||||
0.571,
|
||||
1.7,
|
||||
1.702,
|
||||
0.573
|
||||
],
|
||||
"footprint": [
|
||||
0.57,
|
||||
0.57
|
||||
]
|
||||
],
|
||||
"hi_variant": "procity_fit_mic_stand_01_hi.glb"
|
||||
}
|
||||
],
|
||||
"errors": []
|
||||
|
||||
@ -33,7 +33,11 @@ AUDIO_MUSIC = { # interior music key -> (gain, shop types, note)
|
||||
"milkbar": (0.50, ["milkbar", "general"], "milk bar radio"),
|
||||
"video-synth": (0.50, ["video"], "video store synth hum"),
|
||||
"arcade": (0.50, ["arcade", "dept"], "arcade cabinet"),
|
||||
"pubrock-live":(0.60, ["pub"], "gig live bed — F plays via room.audio gigKey when a gig is on"),
|
||||
# ── gig beds: the manifest key IS the gigKey, ALWAYS `gig-<genreKey>` (R13 debt #1; one key,
|
||||
# zero mapping). C emits room.audio.gigKey='gig-'+genreKey, F/B read this key directly. ──
|
||||
"gig-pubrock": (0.60, ["pub"], "gig live bed — pub-rock (pub); F plays via room.audio gigKey"),
|
||||
"gig-grunge": (0.60, ["band_room"], "gig live bed — grunge (band_room)"),
|
||||
"gig-covers": (0.50, ["rsl"], "gig live bed — covers (RSL); mellower, organ-led"),
|
||||
}
|
||||
AUDIO_INTERIOR_AMB = {"crowd-walla": (0.4, "gig crowd layer (interior; F plays when gig on)")} # R12
|
||||
AUDIO_SFX = {"doorbell": 0.7, "door-open": 0.6, "till": 0.7, "riffle": 0.6, # one-shots
|
||||
@ -147,8 +151,9 @@ def build_audio():
|
||||
footsteps[surf] = vs
|
||||
if footsteps:
|
||||
sfx["footstep"] = footsteps
|
||||
if "pubrock-live" in music: # R12: flag gig-only assets for F
|
||||
music["pubrock-live"]["gig"] = True
|
||||
for k in music: # flag gig-only beds for F (any 'gig-*' key)
|
||||
if k.startswith("gig-"):
|
||||
music[k]["gig"] = True
|
||||
if "applause" in sfx:
|
||||
sfx["applause"]["gig"] = True
|
||||
return {"ambience": ambience, "sfx": sfx, "music": music}
|
||||
|
||||
152
pipeline/decimate_props.py
Normal file
@ -0,0 +1,152 @@
|
||||
"""PROCITY prop decimation — collapse-decimate a normalized GLB to a tri budget while KEEPING its
|
||||
original materials + UVs (no voxel-remesh, no re-bake). The detail-preserving path for props whose
|
||||
thin features (mic pole, cymbal stands, guitar necks) a voxel-remesh would fatten.
|
||||
|
||||
Runs headless on this box's Blender:
|
||||
BL=/Applications/Blender.app/Contents/MacOS/Blender
|
||||
"$BL" --background --python decimate_props.py -- BATCH.json OUT_DIR [THUMB_DIR]
|
||||
|
||||
BATCH.json: [ {"src":"web/assets/models/procity_fit_guitar_amp_01.glb",
|
||||
"id":"guitar_amp", "category":"fit", "tris":14000} , ... ]
|
||||
|
||||
vs bake_lowpoly.py: that one throws topology away (voxel remesh → bake) to beat the multi-shell floor
|
||||
on boxy hero props; this one collapses edges in place to keep silhouettes on thin/among-shell props.
|
||||
Pick per asset by eyeballing the thumbs — anything that floors above budget or reads wrong here goes
|
||||
to bake_lowpoly.py instead. Same house GLB law out: metres, +Y up, base-origin, WebP, no Draco.
|
||||
"""
|
||||
import bpy, sys, os, json, math
|
||||
from mathutils import Vector
|
||||
|
||||
ARGV = sys.argv[sys.argv.index("--") + 1:]
|
||||
BATCH, OUT_DIR = ARGV[0], (ARGV[1] if len(ARGV) > 1 else "/tmp/procity_decim")
|
||||
THUMB_DIR = ARGV[2] if len(ARGV) > 2 else os.path.join(OUT_DIR, "thumbs")
|
||||
os.makedirs(OUT_DIR, exist_ok=True); os.makedirs(THUMB_DIR, exist_ok=True)
|
||||
|
||||
|
||||
def wipe():
|
||||
for o in list(bpy.data.objects):
|
||||
bpy.data.objects.remove(o, do_unlink=True)
|
||||
for coll in (bpy.data.meshes, bpy.data.materials, bpy.data.images):
|
||||
for d in list(coll):
|
||||
if d.users == 0:
|
||||
coll.remove(d)
|
||||
|
||||
|
||||
def import_join(src):
|
||||
bpy.ops.import_scene.gltf(filepath=src)
|
||||
meshes = [o for o in bpy.data.objects if o.type == 'MESH']
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
for m in meshes:
|
||||
m.select_set(True)
|
||||
bpy.context.view_layer.objects.active = meshes[0]
|
||||
if len(meshes) > 1:
|
||||
bpy.ops.object.join()
|
||||
obj = bpy.context.view_layer.objects.active
|
||||
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
|
||||
return obj
|
||||
|
||||
|
||||
def bounds(obj):
|
||||
bb = [obj.matrix_world @ Vector(c) for c in obj.bound_box]
|
||||
mn = Vector((min(v.x for v in bb), min(v.y for v in bb), min(v.z for v in bb)))
|
||||
mx = Vector((max(v.x for v in bb), max(v.y for v in bb), max(v.z for v in bb)))
|
||||
return mn, mx
|
||||
|
||||
|
||||
def tri_count(obj):
|
||||
"""Honest triangle count (n-gons split): sum of (loop_total - 2) per polygon."""
|
||||
return sum(len(p.vertices) - 2 for p in obj.data.polygons)
|
||||
|
||||
|
||||
def decimate(obj, target):
|
||||
# triangulate first so the collapse ratio targets triangles honestly, then collapse-decimate.
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
obj.select_set(True); bpy.context.view_layer.objects.active = obj
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
bpy.ops.mesh.select_all(action='SELECT')
|
||||
bpy.ops.mesh.quads_convert_to_tris()
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
n = len(obj.data.polygons)
|
||||
if n > target:
|
||||
dm = obj.modifiers.new("dec", 'DECIMATE')
|
||||
dm.decimate_type = 'COLLAPSE'
|
||||
dm.ratio = max(0.01, target / n)
|
||||
dm.use_collapse_triangulate = True
|
||||
bpy.ops.object.modifier_apply(modifier=dm.name)
|
||||
return n
|
||||
|
||||
|
||||
def base_origin(obj):
|
||||
mn, mx = bounds(obj)
|
||||
obj.location -= Vector(((mn.x + mx.x) / 2, (mn.y + mx.y) / 2, mn.z))
|
||||
bpy.ops.object.transform_apply(location=True)
|
||||
|
||||
|
||||
def render_thumb(obj, png_name):
|
||||
scn = bpy.context.scene
|
||||
mn, mx = bounds(obj); ctr = (mn + mx) / 2
|
||||
size = max((mx - mn).x, (mx - mn).y, (mx - mn).z) or 1.0
|
||||
for o in list(bpy.data.objects):
|
||||
if o.type in ('CAMERA', 'LIGHT'):
|
||||
bpy.data.objects.remove(o, do_unlink=True)
|
||||
w = bpy.data.worlds.get("tw") or bpy.data.worlds.new("tw")
|
||||
scn.world = w; w.use_nodes = True
|
||||
w.node_tree.nodes["Background"].inputs[1].default_value = 1.05
|
||||
bpy.ops.object.light_add(type='AREA', location=(ctr.x + size, ctr.y - size, mx.z + size))
|
||||
bpy.context.object.data.energy = 800 * size; bpy.context.object.data.size = 6
|
||||
bpy.ops.object.camera_add(); cam = bpy.context.object; scn.camera = cam
|
||||
r = size * 1.9
|
||||
cam.location = Vector((ctr.x + r * 0.8, ctr.y - r, ctr.z + size * 0.6))
|
||||
cam.rotation_euler = (ctr - cam.location).to_track_quat('-Z', 'Y').to_euler()
|
||||
scn.render.engine = 'BLENDER_WORKBENCH'
|
||||
scn.display.shading.light = 'STUDIO'; scn.display.shading.color_type = 'TEXTURE'
|
||||
scn.display.shading.show_cavity = True
|
||||
scn.view_settings.view_transform = 'Standard'
|
||||
scn.render.film_transparent = True
|
||||
scn.render.resolution_x = scn.render.resolution_y = 256
|
||||
p = os.path.join(THUMB_DIR, png_name); scn.render.filepath = p
|
||||
bpy.ops.render.render(write_still=True)
|
||||
return p
|
||||
|
||||
|
||||
def decimate_one(spec):
|
||||
wipe()
|
||||
src = spec["src"] if os.path.isabs(spec["src"]) else os.path.join(os.getcwd(), spec["src"])
|
||||
obj = import_join(src)
|
||||
tri_before = tri_count(obj)
|
||||
decimate(obj, spec.get("tris", 14000))
|
||||
base_origin(obj)
|
||||
tri_after = tri_count(obj)
|
||||
name = f"procity_{spec['category']}_{spec['id']}_01.glb"
|
||||
out = os.path.join(OUT_DIR, name)
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
obj.select_set(True); bpy.context.view_layer.objects.active = obj
|
||||
bpy.ops.export_scene.gltf(filepath=out, export_format='GLB', export_yup=True,
|
||||
use_selection=True, export_image_format='WEBP', export_image_quality=88)
|
||||
thumb = render_thumb(obj, name.replace(".glb", ".png"))
|
||||
mn, mx = bounds(obj)
|
||||
# Blender is Z-up: height is Z, the ground footprint is X×Y (matches bake_lowpoly.py + _gig_results).
|
||||
return {"file": name, "tri_before": tri_before, "tri_after": tri_after,
|
||||
"size_m": [round(mx.x - mn.x, 3), round(mx.z - mn.z, 3), round(mx.y - mn.y, 3)],
|
||||
"footprint": [round(mx.x - mn.x, 2), round(mx.y - mn.y, 2)], "thumb": thumb, "out": out}
|
||||
|
||||
|
||||
def main():
|
||||
specs = json.load(open(BATCH))
|
||||
results, errors = [], []
|
||||
for spec in specs:
|
||||
try:
|
||||
r = decimate_one(spec)
|
||||
results.append(r)
|
||||
print(f"OK {r['file']:40} {r['tri_before']:>6}→{r['tri_after']:>5} tris "
|
||||
f"footprint {r['footprint']} h {r['size_m'][1]}")
|
||||
except Exception as e:
|
||||
import traceback; traceback.print_exc()
|
||||
errors.append({"id": spec.get("id"), "error": str(e)})
|
||||
print(f"ERR {spec.get('id')}: {e}")
|
||||
json.dump({"results": results, "errors": errors},
|
||||
open(os.path.join(OUT_DIR, "_decim_results.json"), "w"), indent=2)
|
||||
print(f"\nDECIM_DONE ok={len(results)} err={len(errors)} → {OUT_DIR}")
|
||||
|
||||
|
||||
main()
|
||||
@ -248,6 +248,16 @@ def pad(midi, dur, detune=0.008):
|
||||
return x * adsr(dur, 0.4, 0.2, 0.8, 0.5)
|
||||
|
||||
|
||||
def organ(midi, dur):
|
||||
# drawbar-organ-ish: stacked harmonic sines (16'/8'/5⅓'/4'/2') + a gentle Leslie tremolo and a
|
||||
# soft percussive attack. Warm and mellow — the RSL covers-band voice.
|
||||
f = midi_hz(midi); t = tarr(dur)
|
||||
drawbars = [(0.5, 1), (1.0, 2), (0.55, 3), (0.4, 4), (0.22, 6)]
|
||||
x = sum(a * np.sin(2 * np.pi * f * mult * t) for a, mult in drawbars) / 2.6
|
||||
trem = 1 + 0.06 * np.sin(2 * np.pi * 5.2 * t) # gentle Leslie tremolo
|
||||
return x * trem * adsr(dur, 0.012, 0.15, 0.82, 0.12)
|
||||
|
||||
|
||||
def kick(dur=0.28):
|
||||
n = int(dur * SR)
|
||||
f = np.linspace(120, 45, n)
|
||||
@ -602,6 +612,16 @@ def powerchord(root, dur, drive=3.5):
|
||||
return x * adsr(dur, 0.003, 0.05, 0.7, 0.04)
|
||||
|
||||
|
||||
def grungechord(root, dur, drive=5.0):
|
||||
# sludgier, down-tuned cousin of powerchord: root+5th+octave, a square sub-octave for weight,
|
||||
# heavier fuzz and a darker filter. `drive` swings the quiet-loud grunge dynamic.
|
||||
x = (osc(midi_hz(root), dur, "saw") + osc(midi_hz(root+7), dur, "saw")
|
||||
+ osc(midi_hz(root+12), dur, "saw") + 0.6 * osc(midi_hz(root-12), dur, "square"))
|
||||
x = softclip(x * 0.5, drive) # thick fuzz
|
||||
x = biquad_lp(x, 2400, 0.9) # darker than pub-rock's 3200
|
||||
return x * adsr(dur, 0.004, 0.06, 0.75, 0.06)
|
||||
|
||||
|
||||
def music_pubrock(name):
|
||||
# the gig live bed — 90s Aussie pub-rock: driving power chords, punchy bass, rock kit. Louder
|
||||
# + rawer than the shop beds. Original I-bVII-IV-I riff in E.
|
||||
@ -617,6 +637,51 @@ def music_pubrock(name):
|
||||
return loop_music(body, 0.06)
|
||||
|
||||
|
||||
def music_grunge(name):
|
||||
# band-room gig bed — 90s grunge: down-tuned sludge riff with a quiet-loud dynamic (clean-ish
|
||||
# verse → full-fuzz chorus) and a loose, heavy kit. Original i-bVII-bVI riff, low + dark. Slower
|
||||
# and murkier than the pub-rock bed so the two venues never sound the same.
|
||||
r = rng(name); bpm = 112
|
||||
prog = [35, 35, 42, 40] # 4 bars (B B E D-ish), played verse then chorus
|
||||
verse = render_line([(root, 0.5) for root in prog for _ in range(8)], # palm-mute 8ths, cleanish
|
||||
bpm, lambda m, d: grungechord(m, d, 2.4) * 0.32, gap=0.03)
|
||||
chorus = render_line([(root, 0.5) for root in prog for _ in range(8)], # full sludge
|
||||
bpm, lambda m, d: grungechord(m, d, 5.5) * 0.55, gap=0.0)
|
||||
guitar = np.concatenate([verse, chorus])
|
||||
bass = render_line([(root - 12, 0.5) for root in prog for _ in range(8)] * 2,
|
||||
bpm, lambda m, d: synth_note(m, d, "saw", 0.004, 0.06, 0.7, 0.04, cutoff=520))
|
||||
vdr = render_drums({"k": [0, 8], "s": [4, 12], "h": [0, 4, 8, 12]}, bpm, 4, r) # sparse verse
|
||||
cdr = render_drums({"k": [0, 3, 6, 8, 11, 14], "s": [4, 12],
|
||||
"h": [0, 2, 4, 6, 8, 10, 12, 14], "o": [7, 15]}, bpm, 4, r) # driving chorus
|
||||
drums = np.concatenate([vdr, cdr])
|
||||
body = mix(pan(guitar, -0.1)*0.72, pan(bass, 0.08)*0.62, pan(drums, 0)*0.62) # headroom: dense chorus
|
||||
return loop_music(body, 0.10)
|
||||
|
||||
|
||||
def music_covers(name):
|
||||
# RSL gig bed — a covers band at the returned-servicemen's club: mellow, organ-led easy-listening.
|
||||
# Warm I-vi-IV-V turnaround (original voicing), walking bass, soft kit, a sparse organ answer.
|
||||
# Cleaner and warmer than the pub/band-room beds — the crowd-cap stress room reads relaxed.
|
||||
r = rng(name); bpm = 100; bars = 8
|
||||
spb = 60 / bpm
|
||||
prog = [48, 45, 53, 55] # C Am F G7 (roots), one per bar, ×2 = 8 bars
|
||||
chords = {48: [0, 4, 7], 45: [0, 3, 7], 53: [0, 4, 7], 55: [0, 4, 7, 10]} # G7 on the turnaround
|
||||
org_bars = []
|
||||
for root in prog * 2: # sustained organ triad per bar
|
||||
ch = mix(*[organ(root + 12 + iv, 4 * spb) for iv in chords[root]]) * 0.5
|
||||
cell = int(4 * spb * SR)
|
||||
org_bars.append(np.concatenate([ch, np.zeros(max(cell - len(ch), 0))])[:cell])
|
||||
organ_part = np.concatenate(org_bars)
|
||||
walk = sum([[(root - 12, 1.0), (root - 5, 1.0), (root, 1.0), (root - 3, 1.0)] for root in prog * 2], [])
|
||||
bass = render_line(walk, bpm, lambda m, d: synth_note(m, d, "tri", 0.006, 0.12, 0.6, 0.06, cutoff=800))
|
||||
drums = render_drums({"k": [0, 8], "s": [4, 12], "h": [0, 2, 4, 6, 8, 10, 12, 14]}, bpm, bars, r) * 0.5
|
||||
lead_notes = sum([[(root + 12, 1.5), (None, 0.5), (root + 19, 1.0), (None, 1.0)] for root in prog * 2], [])
|
||||
lead = render_line(lead_notes, bpm, # gentle organ-ish answer
|
||||
lambda m, d: synth_note(m, d, "tri", 0.02, 0.12, 0.5, 0.2, cutoff=1600, vib=0.8))
|
||||
body = mix(pan(organ_part, -0.1)*0.7, pan(bass, 0.05)*0.8, pan(drums, 0)*0.5, pan(lead, 0.25)*0.3)
|
||||
return loop_music(body, 0.18) # warmer reverb than the rock beds
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# the pack: name -> (category, generator, {loop, gain, note})
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@ -648,8 +713,11 @@ PACK = {
|
||||
"music-milkbar": ("music", music_milkbar, dict(loop=True, gain=0.5, note="milk bar radio")),
|
||||
"music-video-synth": ("music", music_video_synth, dict(loop=True, gain=0.5, note="video store")),
|
||||
"music-arcade": ("music", music_arcade, dict(loop=True, gain=0.5, note="arcade")),
|
||||
# ── round-12 gig layer (?gigs=1 consumers: F plays these when a gig is on) ────────────
|
||||
"music-pubrock-live": ("music", music_pubrock, dict(loop=True, gain=0.6, note="gig live bed — pub-rock; F plays via room.audio gigKey")),
|
||||
# ── gig layer (?gigs=1 consumers: F plays these when a gig is on) — the manifest key IS the
|
||||
# gigKey, ALWAYS `gig-<genreKey>` (R13 debt #1: one key, zero mapping). One bed per venue kind. ──
|
||||
"music-gig-pubrock": ("music", music_pubrock, dict(loop=True, gain=0.6, note="gig live bed — pub-rock (pub); F plays via room.audio gigKey")),
|
||||
"music-gig-grunge": ("music", music_grunge, dict(loop=True, gain=0.6, note="gig live bed — grunge (band_room)")),
|
||||
"music-gig-covers": ("music", music_covers, dict(loop=True, gain=0.5, note="gig live bed — covers (RSL); mellower, organ-led")),
|
||||
"ambience-crowd-walla":("ambience", amb_crowd_walla, dict(loop=True, gain=0.4, note="gig crowd layer (interior)")),
|
||||
"sfx-applause": ("sfx", sfx_applause, dict(loop=False, gain=0.7, note="between-sets cheer")),
|
||||
}
|
||||
|
||||
BIN
web/assets/audio/music-gig-covers.m4a
Normal file
BIN
web/assets/audio/music-gig-covers.ogg
Normal file
BIN
web/assets/audio/music-gig-grunge.m4a
Normal file
BIN
web/assets/audio/music-gig-grunge.ogg
Normal file
BIN
web/assets/audio/music-gig-pubrock.m4a
Normal file
BIN
web/assets/audio/music-gig-pubrock.ogg
Normal file
@ -776,7 +776,7 @@
|
||||
"file": "procity_fit_bass_guitar_01.glb",
|
||||
"footprint": [
|
||||
0.34,
|
||||
0.12
|
||||
0.13
|
||||
],
|
||||
"height": 1.15,
|
||||
"thumb": "thumbs/procity_fit_bass_guitar_01.png"
|
||||
@ -793,10 +793,10 @@
|
||||
"guitar_amp": {
|
||||
"file": "procity_fit_guitar_amp_01.glb",
|
||||
"footprint": [
|
||||
0.51,
|
||||
0.52,
|
||||
0.29
|
||||
],
|
||||
"height": 0.6,
|
||||
"height": 0.607,
|
||||
"thumb": "thumbs/procity_fit_guitar_amp_01.png"
|
||||
},
|
||||
"mic_stand": {
|
||||
@ -805,7 +805,7 @@
|
||||
0.57,
|
||||
0.57
|
||||
],
|
||||
"height": 1.7,
|
||||
"height": 1.702,
|
||||
"thumb": "thumbs/procity_fit_mic_stand_01.png"
|
||||
},
|
||||
"cash_register": {
|
||||
@ -1115,15 +1115,37 @@
|
||||
],
|
||||
"note": "arcade cabinet"
|
||||
},
|
||||
"pubrock-live": {
|
||||
"file": "audio/music-pubrock-live.ogg",
|
||||
"fallback": "audio/music-pubrock-live.m4a",
|
||||
"gig-pubrock": {
|
||||
"file": "audio/music-gig-pubrock.ogg",
|
||||
"fallback": "audio/music-gig-pubrock.m4a",
|
||||
"loop": true,
|
||||
"gain": 0.6,
|
||||
"types": [
|
||||
"pub"
|
||||
],
|
||||
"note": "gig live bed \u2014 F plays via room.audio gigKey when a gig is on",
|
||||
"note": "gig live bed \u2014 pub-rock (pub); F plays via room.audio gigKey",
|
||||
"gig": true
|
||||
},
|
||||
"gig-grunge": {
|
||||
"file": "audio/music-gig-grunge.ogg",
|
||||
"fallback": "audio/music-gig-grunge.m4a",
|
||||
"loop": true,
|
||||
"gain": 0.6,
|
||||
"types": [
|
||||
"band_room"
|
||||
],
|
||||
"note": "gig live bed \u2014 grunge (band_room)",
|
||||
"gig": true
|
||||
},
|
||||
"gig-covers": {
|
||||
"file": "audio/music-gig-covers.ogg",
|
||||
"fallback": "audio/music-gig-covers.m4a",
|
||||
"loop": true,
|
||||
"gain": 0.5,
|
||||
"types": [
|
||||
"rsl"
|
||||
],
|
||||
"note": "gig live bed \u2014 covers (RSL); mellower, organ-led",
|
||||
"gig": true
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |