s&box Package Code Search

Search C# source code, UI razor templates, shaders, and configs across s&box packages.

Showing code results for query: * (78 total matches found)
gamah.skafinity / Code/Engine/Drums/Groove.cs
Game library
using System;
using System.Collections.Generic;

using static Skafinity.Osc;

namespace Skafinity;

/// <summary>
/// One drum groove: what the kick, the snare and the cymbal play.
///
/// Grooves used to be five cases in a <c>switch</c>, and rock, country AND punk all resolved to
/// the same <c>default</c> straight backbeat — three of six genres playing identical drums under
/// different guitars. A groove is a set of patterns now, the same way harmony is a set of
/// tables, and each genre draws from its own.
///
/// Cell values: the kick has none (an onset is a kick). A snare cell is 0 for a hit and
/// <see cref="Ghost"/> for a ghost note. A cymbal cell is 0 for the closed/bow articulation and
/// <see cref="Open"/> for the open hat / ride bell — which of the two instruments plays is the
/// section's hats-or-ride roll, not the groove's business.
///
/// WHAT IS MEASURED IS THE TABLE, AND WHAT SHIPS IS ARRANGED FROM IT. Read this before quoting
/// any number below. The placements were fitted to a played corpus and they are real; the engine
/// then draws a groove per SECTION and works on its kick and snare against the section's skeleton
/// (see <see cref="MusicGen.ArrangeKit"/>), so a bar that reaches a listener is a mutation of a
/// measured pattern rather than the pattern itself. Three things follow and they are separable:
///
///   * <b>the tables are measured SEED material.</b> The placements are real, the three
///     corrections below are still why these tables look the way they do, and the arranger never
///     invents a gesture the genre does not have.
///   * <b>what the engine plays is a design call</b>, bounded by the SPINE (see
///     <see cref="SpineOf"/>) — the struck backbeat and the downbeat kick, which mutation may not
///     reach, so a genre's identity survives being arranged.
///   * <b>the accent weights are untouched and remain measured.</b> Velocity was a separate
///     question off the same pass and nothing about arranging placements reaches it.
///
/// RESTORING THE MEASURED OCCUPANCY WAS CONSIDERED AND LOST, and this is recorded because the
/// paragraph below is what will make a future session rediscover it. The pass read a DISTRIBUTION
/// — what fraction of bars carry each drum at each position — and then thresholded it to binary
/// cells, so the variance was measured and thrown away at authoring time; drawing the cells from
/// those probabilities instead would give bar-to-bar variation that is the corpus's own rather
/// than anyone's invention, and the near-certain positions would be a genre guard for free.
/// (<see cref="MusicGen.FootOccupancy"/> is the one place it survives.) It lost on three counts:
/// it caps variety at whatever the dataset's variance happens to be, it needs a fresh pass over
/// Groove MIDI that neither this repo nor its tooling contains, and metal is not in the dataset at
/// all. It is the fallback if free mutation ever turns out to wreck the genres, and it is scoped.
///
/// This distinction is <see cref="GenreProfile.FillHits"/>'s, and it is here for the reason that
/// block gives in its own words: a sentence in this register is READ as a measurement, so leaving
/// the header saying "where the hits fall is measured" would launder an arrangement into a
/// citation.
///
/// WHERE THE TABLES' HITS FALL IS MEASURED, the same way the accent weights in
/// <see cref="GenreProfile"/> are, off the same source: Google Magenta's Groove MIDI Dataset
/// (CC BY 4.0; verified 2026-08-02, https://magenta.tensorflow.org/datasets/groove). Method, since
/// neither the dataset nor the reader is in this repo: fold every note-on of every 4/4 performance
/// of a style onto one bar at the nearest sixteenth and read each drum's OCCUPANCY per metric
/// position — what fraction of bars carry that drum there. Occupancy answers placement; velocity
/// answered the accents. The two are separate questions off one pass.
///
/// The three placements that disagreed with the tables, and what each one moved:
///   * country hi-hat — ~84% on the OFFBEAT eighth against ~36% on the beat, while both country
///     grooves put the cymbal on the pulse and nowhere else. The "chick" is the & and the tables
///     had it on the beat, which is the single largest mismatch the pass turned up.
///   * rock kick — far more &-of-1 and &-of-3 than the two-bar backbeat spent, so each bar of the
///     pair gains one pushed kick.
///   * punk snare — measures on very nearly every eighth rather than on 2 and 4 alone. It is the
///     train beat's vocabulary at punk's tempo: the backbeat is struck and everything between it
///     is ghosted.
///
/// MIND THE SAMPLE SIZES, which are wildly uneven and travel with any figure taken from here: rock
/// is 6521 bars and settles rock, punk 278, country 120 from two performances. Country's is thin
/// enough that the 84/36 split is an INDICATION — it is acted on because the direction is
/// unambiguous and the table said the opposite, not because 120 bars settle a number.
/// </summary>
sealed class DrumGroove
{
	public const int Ghost = 1;

	// ── The cymbal hand's vocabulary ──
	// Open KEEPS THE VALUE 1, so every table above means exactly what it meant when 0-or-open was
	// the whole language. A hi-hat is a pedal and a pedal is a distance, so what a cell says is
	// how far open — except for the two articulations that are not a distance at all: the foot
	// closing the cymbals with no stick on them, and the foot opening and shutting them again.
	public const int Open = 1;
	/// <summary>Half open: the "sloshy" hat. It is not the midpoint of a switch — see RenderHat's
	/// geometric map, which is what makes this a position a foot can actually hold.</summary>
	public const int Half = 2;
	/// <summary>The foot chick: the hat speaking on its own, with no stick involved.</summary>
	public const int Foot = 3;
	/// <summary>Foot splash: opened and shut in one motion.</summary>
	public const int Splash = 4;

	public string Name { get; init; }
	public Pattern Kick { get; init; }
	public Pattern Snare { get; init; }
	public Pattern Cymbal { get; init; }

	/// <summary>Extra ghost-note propensity on top of what the pattern names — the "busy" layer's
	/// scaling factor for this groove.</summary>
	public float GhostRate { get; init; } = 1f;

	/// <summary>Chance of a crash on the section's first downbeat.</summary>
	public float CrashOnOne { get; init; } = 0.35f;

	/// <summary>
	/// THE SPINE: which of a groove's onsets ARE the genre, and so may not be dropped or moved.
	///
	/// This is the drums' answer to <see cref="CellClass"/>, and it is what stops arranging the kit
	/// from eroding the three measured tells the corpus pass corrected. It is a LAW rather than a
	/// per-groove list of ticks, because a list is a table that has to be re-authored every time a
	/// groove is added and gets it wrong silently when nobody does:
	///
	///   * <b>every STRUCK snare</b>. The backbeat is where a genre puts it — 2 and 4 in most of
	///     them, 3 alone in pop's half-time — and a rule phrased in beat numbers would be wrong for
	///     whichever genre disagrees. The ghosts around it are the density and stay arrangeable,
	///     which is the whole of what punk's snare has to say: strike two, ghost the rest.
	///   * <b>every kick ON A BEAT.</b> A KICK ON THE BEAT IS THE PULSE; A KICK OFF IT IS THE PUSH,
	///     and the push is the thing a drummer varies. Protecting the bar's first beat alone was
	///     not enough and the failure was specific rather than general: beat 1 held at 96–97% in
	///     every genre while every OTHER anchor eroded — country's beat 3, half of boom-chick, went
	///     missing in 23% of bars, pop's beat 4 in 24%, rock's beat 3 in 15%. The kick count per
	///     bar barely moved, so nothing about its level or its density said so; what a listener
	///     gets is a kick that flickers where the pulse should be.
	///
	/// AND A GROOVE'S IDENTITY IS PARTLY WHERE IT DOES NOT PLAY, which a rule about onsets cannot
	/// say on its own. The one drop IS the hole on beat 1, so <see cref="MusicGen.Add"/> may not
	/// put a kick on a beat either — same law read the other way round, and without it ska's beat-1
	/// occupancy climbed 41% → 45% as one-drop bars quietly acquired the downbeat they are defined
	/// by not having. On the beat is the groove; off the beat is the arrangement.
	///
	/// The cymbal has no spine here because the cymbal is not arranged at all: it is the pulse, and
	/// country's hat on the "and" — the largest mismatch the corpus pass found — is preserved by
	/// construction rather than by a rule that could be got wrong.
	/// </summary>
	public static bool[] SpineOf( Pattern p, bool kick, int barTicks )
	{
		if ( p == null ) return null;
		var spine = new bool[p.Count];
		for ( int i = 0; i < p.Count; i++ )
			spine[i] = kick ? IsPulse( p.TickAt( i ) ) : p.ValueAt( i ) != Ghost;
		return spine;
	}

	/// <summary>A tick the kick's spine lives on — see <see cref="SpineOf"/>. One law, read two
	/// ways: an onset here may not be dropped or moved, and an onset may not be ADDED here.
	/// </summary>
	public static bool IsPulse( int tick ) => tick % Timing.TicksPerBeat == 0;

	const int R = Harmony.Rest;
	static Pattern E( params int[] c ) => Pattern.Eighths( c );
	static Pattern S( params int[] c ) => Pattern.Sixteenths( c );

	// ── Ska-punk ──
	public static readonly DrumGroove[] SkaPunk =
	{
		new()
		{
			Name = "one drop",
			// The one drop: nothing on beat 1 at all. Kick and snare land together on beat 3, and
			// the space where the downbeat should be is the whole point of the feel.
			Kick = E( R, R, R, R, 0, R, R, R ),
			Snare = E( R, R, Ghost, R, 0, R, R, R ),
			Cymbal = E( 0, 0, 0, 0, 0, 0, 0, Open ),
			GhostRate = 0.8f, CrashOnOne = 0.25f,
		},
		new()
		{
			Name = "steppers",
			// Steppers: a kick on every beat — the four-to-the-floor of reggae, and what a ska
			// song reaches for when it wants to drive rather than lope.
			Kick = E( 0, R, 0, R, 0, R, 0, R ),
			Snare = E( R, R, 0, R, R, R, 0, R ),
			Cymbal = E( 0, 0, 0, 0, 0, 0, 0, Open ),
		},
	};

	// ── Rock ──
	public static readonly DrumGroove[] Rock =
	{
		new()
		{
			Name = "backbeat",
			// Two bars, because a rock backbeat that is byte-identical every bar is a drum
			// machine. Each bar carries one pushed kick and it is a different one: bar 1 pushes the
			// "and of 1", bar 2 the "and of 2" into the "and of 3". Those pushes are the measured
			// shape — a rock kick spends far more of its bar on &1 and &3 than two anchor hits.
			Kick = E( 0, 0, R, R, 0, R, R, R,
			          0, R, R, 0, 0, 0, R, R ),
			Snare = E( R, R, 0, R, R, R, 0, R,
			           R, R, 0, R, R, R, 0, R ),
			Cymbal = E( 0, 0, 0, 0, 0, 0, 0, Open ),
		},
		new()
		{
			Name = "driving eights",
			Kick = E( 0, R, R, 0, 0, R, R, R,
			          0, R, R, 0, 0, R, 0, R ),
			Snare = E( R, R, 0, R, R, R, 0, R,
			           R, R, 0, R, R, R, 0, Ghost ),
			Cymbal = E( 0, 0, 0, 0, 0, 0, 0, 0 ),
			GhostRate = 1.2f,
		},
	};

	// ── Country ──
	public static readonly DrumGroove[] Country =
	{
		new()
		{
			Name = "train beat",
			// The train beat: a constant running snare, ghosted everywhere except the backbeat,
			// which is the sound of country drumming and did not exist in this engine at all.
			Kick = E( 0, R, R, R, 0, R, R, R ),
			Snare = S( Ghost, Ghost, Ghost, Ghost, 0, Ghost, Ghost, Ghost,
			           Ghost, Ghost, Ghost, Ghost, 0, Ghost, Ghost, Ghost ),
			// The hat is on the "and". Two bars to hold the measured split without a per-hit roll:
			// 3 of 8 beats carry the hat against 7 of 8 offbeats, which is the 36/84 the dataset
			// reads. On the pulse it was the one thing in the kit contradicting the genre's own
			// accent weight — country leans on the offbeat and had nothing there to lean on.
			Cymbal = E( 0, 0, R, 0, R, 0, R, 0,
			            R, 0, 0, 0, 0, 0, R, R ),
			GhostRate = 0.6f, CrashOnOne = 0.2f,
		},
		new()
		{
			Name = "two beat",
			// The other country feel: a two-beat "boom-chick" where the kit gets out of the way
			// of the bass and the guitar entirely.
			Kick = E( 0, R, R, R, 0, R, R, R ),
			Snare = E( R, R, 0, R, R, R, 0, R ),
			// Lighter than the train beat's hat and still offbeat-led: the "chick" of boom-chick is
			// the & whichever country feel is playing, and this one just plays fewer of them.
			Cymbal = E( 0, 0, R, R, 0, 0, R, Open ),
			GhostRate = 0.5f, CrashOnOne = 0.15f,
		},
	};

	// ── Metal ──
	public static readonly DrumGroove[] Metal =
	{
		new()
		{
			Name = "double kick",
			// DOUBLE KICK IS A BURST, NOT A SETTING. One bar of unbroken sixteenths looped for
			// three minutes is ~13 hits a second with nothing ever changing, which is why it read
			// as a blast beat at every tempo and every subdivision: the tell is not the rate, it
			// is that the rate never moves. A drummer rides an ordinary kick pattern and stands on
			// the double pedal under a riff — for a beat into a bar line, for a bar at the top of
			// a phrase — and the contrast is the whole effect.
			//
			// Four bars, because Pattern carries its own length: bars 1 and 3 are a played metal
			// kick, bar 2 bursts over its last beat, and bar 4 is the full double-kick bar the
			// phrase turns around on. Same one object, no new mechanism.
			Kick = S( 0, R, R, R, 0, R, R, 0, 0, R, R, R, 0, R, 0, R,
			          0, R, R, R, 0, R, R, 0, 0, R, R, R, 0, 0, 0, 0,
			          0, R, R, R, 0, R, R, 0, 0, R, R, R, 0, R, 0, R,
			          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ),
			Snare = E( R, R, 0, R, R, R, 0, R ),
			Cymbal = E( 0, 0, 0, 0, 0, 0, 0, 0 ),
			// The kick no longer fills every sixteenth, so the busy layer has somewhere to sit
			// again — but only just: metal's kit is a wall by design and the ghosts are the mortar.
			GhostRate = 0.2f, CrashOnOne = 0.55f,
		},
		new()
		{
			Name = "thrash",
			// Kick on every eighth under a snare that answers it — faster to read than the
			// double-kick wall, and it leaves the snare somewhere to go.
			Kick = E( 0, 0, 0, 0, 0, 0, 0, 0 ),
			Snare = E( R, R, 0, R, R, R, 0, 0 ),
			Cymbal = E( 0, 0, 0, 0, 0, 0, 0, 0 ),
			GhostRate = 0.3f, CrashOnOne = 0.5f,
		},
		new()
		{
			Name = "two-step",
			// The same beat punk gets, and deliberately the same: bum-tis-bumbum-tis is played in
			// both genres and inventing a heavier variant for metal would be answering a question
			// nobody asked. What separates the two here is everything around it — tempo, kit, the
			// ghost rate below, and the riff on top.
			//
			//         1   e   &   a   2   e   &   a
			//   kick  K   .   .   .   K   K   .   .
			//   snare .   .   S   .   .   .   S   .
			//
			// A separate OBJECT because no two genres may share a groove (the suite asserts it),
			// which is a rule about tables accidentally converging rather than about two genres
			// never playing the same rhythm.
			//
			// WEIGHTED BEHIND EACH GENRE'S OWN GROOVE, on purpose. This one was added because a
			// listener said it was missing, and the check on that kind of change is not whether the
			// reason was good — it was, the beat really was absent from both tables — but whether
			// the WEIGHT came from the same evidence. It did not: joint-top billing was a choice,
			// and it pushed "eighth drive", which this file calls the punk engine, from 60% of punk
			// songs to 37%. A genre's signature stays its most common groove and a new arrival
			// earns its share; ~29% is present without displacing anything.
			Kick = S( 0, R, R, R, 0, 0, R, R,
			          0, R, R, R, 0, 0, R, R ),
			Snare = S( R, R, 0, R, R, R, 0, R,
			           R, R, 0, R, R, R, 0, R ),
			Cymbal = E( 0, 0, 0, 0, 0, 0, 0, 0 ),
			GhostRate = 0.25f, CrashOnOne = 0.5f,
		},
	};

	// ── Punk ──
	public static readonly DrumGroove[] Punk =
	{
		new()
		{
			Name = "eighth drive",
			// The punk engine: eighth-note ride/snare drive at speed. It is not a backbeat with
			// the tempo turned up — the cymbal hand never stops and the kick pushes every beat.
			//
			// The snare hand does not stop either, which is what the measurement says and what the
			// two-hits-a-bar backbeat could not be: 2 and 4 are struck and every eighth between
			// them is ghosted. The energy gate on ghost cells thins it back toward the bare
			// backbeat in a quiet section, so the density is the section's rather than the table's.
			Kick = E( 0, R, 0, R, 0, R, 0, R ),
			Snare = E( Ghost, Ghost, 0, Ghost, Ghost, Ghost, 0, Ghost ),
			Cymbal = E( 0, 0, 0, 0, 0, 0, 0, 0 ),
			GhostRate = 0.4f, CrashOnOne = 0.45f,
		},
		new()
		{
			Name = "d-beat",
			// Early punk, and it stays as it is. Notations of the d-beat vary in where the kick's
			// offbeats sit and this is a legitimate one; it is also NOT the sixteenth gallop the
			// "two-step" entry below carries, which is the beat this table was actually missing.
			Kick = E( 0, R, R, 0, R, 0, R, R,
			          0, R, R, 0, R, 0, R, R ),
			Snare = E( R, R, 0, R, R, R, 0, R,
			           R, R, 0, R, R, R, 0, Ghost ),
			Cymbal = E( 0, 0, 0, 0, 0, 0, 0, 0 ),
			GhostRate = 0.5f, CrashOnOne = 0.5f,
		},
		new()
		{
			Name = "two-step",
			// THE PUNK ENGINE'S OTHER GEAR: kick on the beat, snare on the "&", and the kick
			// doubled at the SIXTEENTH going into every other beat —
			//
			//         1   e   &   a   2   e   &   a
			//   kick  K   .   .   .   K   K   .   .
			//   snare .   .   S   .   .   .   S   .
			//
			// — repeating twice a bar. Drummers call it the two-step or the skank beat; it is not
			// the d-beat (that keeps its snare on 2 and 4, and has its own entry above).
			//
			// IT IS A 2-BEAT CELL AND THAT IS THE WHOLE POINT. The same figure written over four
			// beats — kick, snare, doubled kick on 3, snare — is the identical pattern counted at
			// half the rate, and at this genre's tempo that puts the double every 1.4 s instead of
			// every 0.7 s. It reads as an ordinary rock beat rather than as punk. This engine has
			// been caught by exactly that ambiguity before (see the ska tempo block in
			// GenreProfile): a rhythm means nothing until you say which pulse it is counted against.
			Kick = S( 0, R, R, R, 0, 0, R, R,
			          0, R, R, R, 0, 0, R, R ),
			Snare = S( R, R, 0, R, R, R, 0, R,
			           R, R, 0, R, R, R, 0, R ),
			Cymbal = E( 0, 0, 0, 0, 0, 0, 0, 0 ),
			GhostRate = 0.35f, CrashOnOne = 0.4f,
		},
	};

	// ── Pop ──
	public static readonly DrumGroove[] Pop =
	{
		new()
		{
			Name = "four on the floor",
			Kick = E( 0, R, 0, R, 0, R, 0, R ),
			Snare = E( R, R, 0, R, R, R, 0, R ),
			Cymbal = E( 0, Open, 0, Open, 0, Open, 0, Open ),
			GhostRate = 0.5f, CrashOnOne = 0.3f,
		},
		new()
		{
			Name = "half-time backbeat",
			// The other modern pop feel: the backbeat falls on 3 alone, which halves the pulse
			// without touching the tempo.
			Kick = E( 0, R, R, 0, R, R, 0, R ),
			Snare = E( R, R, R, R, 0, R, R, R ),
			Cymbal = S( 0, R, 0, 0, 0, R, 0, 0, 0, R, 0, 0, 0, R, 0, Open ),
			GhostRate = 0.7f, CrashOnOne = 0.35f,
		},
	};
}

// ── What a fill is made of ──
// Every fill this engine ever played was the same object: `per` evenly-spaced, equally-loud
// hits in EVERY beat of the span, with a floor of four. A two-bar fill was 32 hits that never
// stopped and never moved, which is the blast-beat read — nothing about it was fast, it simply
// occupied every subdivision it could reach. Three things were missing and they are separable.
//
// DENSITY, and it is measured. A real rock fill averages 13.2 hits per bar across the whole
// kit (204 bars of fill performance in the Groove MIDI Dataset — see DrumGroove's header for
// the source and the method), against a FLOOR here of 16 and a 32nd branch of 32. The quietest
// fill this engine could play was already busier than the average real one. The same histogram
// says what the shape of a bar is: the "e" and the "a" carry about 0.62 of the occupancy the
// beats and the "&"s do, so a fill is EIGHTHS WITH SIXTEENTH ORNAMENT, not a sixteenth grid.
// Density is per genre now, like everything else about the kit (GenreProfile.FillHits).
//
// SHAPE, which is what `per` structurally could not express, being one number applied to every
// beat. See FillShape.
//
// DYNAMICS. RenderFill called the kit voices directly and so was the one part of the engine
// with no accent pattern and no energy scaling — a straight exception to the rule that every
// voice routes its level through NoteGain. An even stream of equally-loud hits reads as a wall
// however few of them there are, which is why this is not just a density fix.

/// <summary>The SHAPE of a fill — where its hits sit inside the span. This is the half of a
/// fill that a density number cannot say, and the reason there was only ever one fill.</summary>
enum FillShape
{
	/// <summary>Sparse at the start and filling up into the downbeat it lands on — a bar that
	/// empties itself and then accelerates out of the hole. The commonest fill there is.</summary>
	Ramp,
	/// <summary>Even across the whole span: the roll. What every fill used to be, kept because
	/// it is a real shape and metal is mostly made of it.</summary>
	Rolling,
	/// <summary>Space, then a flurry over the last beat. THIS IS WHERE THE THIRTY-SECOND LIVES
	/// — a pickup is short enough to be played and short enough to stay a gesture, which is
	/// exactly what a bar of unbroken 32nds is not.</summary>
	Pickup,
	/// <summary>Two or three hits with air around them: the tom figure, the single flam, the
	/// bar that does almost nothing. A fill is allowed to be a gesture.</summary>
	Gesture,
}


// The kit's patterns: which drum lands where. The per-song groove, the section's energy, and
// the phrase-end fill.
//
// Part of the MusicGen engine — see MusicGen.cs.

public sealed partial class MusicGen
{
	// ── Drums ──
	// Render one bar of kit off the song's groove. `fillTick` is where a fill takes over (the
	// bar's end tick if there is none), so the groove simply stops there and the fill owns the
	// rest — which is what lets a fill be anything from one beat to two bars.
	void RenderDrumBar( int barTick, int barTicks, int fillTick, Rng noise )
	{
		// Knob ceiling was too frantic: scale so DRUM BUSY 100% reads as the old 75%.
		float busy = Math.Clamp( _c.DrumBusy, 0f, 1f ) * 0.75f * _groove.GhostRate;
		int to = Math.Min( barTick + barTicks, fillTick );
		if ( to <= barTick ) return;

		// The cymbal hand. Which instrument it is was decided per section (_ride); the groove
		// only says where the hits land and which are "open". A thin section HALVES the cymbal
		// pattern rather than playing it quieter — that is what makes a verse read as a verse and
		// a breakdown as a breakdown.
		//
		// Half the ONSETS, not "everything off the beat". Those were the same rule while every
		// groove's cymbal sat on the pulse, and they stop being the same the moment one does not:
		// country's hat is on the "and", so dropping the offbeats there does not thin the kit, it
		// deletes the hi-hat from every verse in the genre. Alternate onsets thin any pattern by
		// half wherever it sits, and for a plain eighth-note cymbal it is exactly what the old rule
		// did.
		// A CRASH ON THE SECTION'S FIRST DOWNBEAT. Every groove has carried a CrashOnOne since the
		// tables were written and nothing ever read it, so no crash landed on any downbeat in the
		// engine — only at the end of a fill and the end of a song. It is one draw, on the one bar
		// it can apply to, so it costs the same from every section's stream.
		// NOT ON THE SONG'S OWN FIRST BAR. A crash PUNCTUATES a transition — it is the drummer
		// marking the seam between one section and the next, and every other place this fires has
		// one. Bar 1 of the intro has nothing behind it to mark, so what lands there is three
		// seconds of cymbal wash over the sparsest section in the song, belonging to no groove and
		// answering nothing that was played. It reads as a cymbal that was already ringing when the
		// song started, which is exactly what it is.
		//
		// The roll still happens, so a genre's draw count does not depend on which bar it is on.
		if ( barTick == _sectionTick && noise.Chance( _groove.CrashOnOne ) && barTick > 0 )
			RenderCrashCym( _time.TickToSample( barTick ),
				_c.CrashVol * KitGain( barTick, 1f, 0.5f ), _crashBright, dark: false );

		bool sparse = _energy < 0.4f;
		// THINNED FIRST, THEN PLAYED, and the slice runs ONE BEAT PAST the bar. An open hat is
		// choked by the next hit the drummer actually plays: half the onsets means half the
		// chokes too, so the thinning cannot happen inside the playing loop — and the hit that
		// chokes an "and of 4" is in the next bar, which is what the lookahead is for. Only the
		// hits inside the bar are played; the rest are read.
		var cym = _groove.Cymbal.Slice( barTick, to + Timing.TicksPerBeat, _sectionTick, _feel );
		var play = new List<Hit>();
		for ( int i = 0, kept = 0; i < cym.Count; i++ )
			if ( !(sparse && (kept++ & 1) == 1) ) play.Add( cym[i] );

		for ( int i = 0; i < play.Count; i++ )
		{
			var h = play[i];
			if ( h.Tick >= to ) break;
			Trace?.Add( TraceVoice.Cymbal, h.Tick );
			int at = _time.TickToSample( h.Tick );
			int next = i + 1 < play.Count ? _time.TickToSample( play[i + 1].Tick ) : int.MaxValue;
			int cell = h.Value;
			// The genre's own accent weight decides how a hat off the beat sits against one on it.
			// A flat 0.75 was a house rule where the measurement is per genre and disagrees in both
			// directions — country and ska lean ON the offbeat, pop's programmed kit buries it.
			float kit = KitGain( h.Tick, h.Vel, 0.55f );
			float amp = _c.HatVol * kit;
			if ( _crashRide )
				// The technique rather than a second cymbal: the hand moves onto a crash and rides
				// it, so the open cell is the accent it leans on rather than an open hat.
				// Crash-riding is a LIFT — the hand moves onto the loudest thing in the kit and the
				// whole section rises. Measured, it was landing within 0.2 dB of an ordinary ride,
				// which is the technique costing a cymbal and buying nothing.
				RenderCrashCym( at, _c.CrashVol * kit * (cell == DrumGroove.Open ? 0.67f : 0.44f),
					_crashDark, dark: true, next, CymbalBands.RestrikeTau );
			else if ( _ride )
				// The bell is a CELL now. It used to be positional — every quarter note was a
				// bell, whatever the groove said — while the open cell a riding section was handed
				// fell through to a hi-hat, so the one thing the pattern said about the cymbal
				// hand was the one thing the ride ignored.
				// EVERY STROKE DAMPS THE ONE BEFORE IT (see RenderCymbal's chokeFloor). A cymbal
				// struck eight times a bar is not eight cymbals summed: the stick is on the metal.
				RenderRideCym( at, amp * RideStroke( h.Tick - barTick ),
					cell == DrumGroove.Open ? _rideBell : _rideBow, next, CymbalBands.RestrikeTau );
			else
				RenderHat( at, HatOpenness( cell ), amp, noise, HatFor( cell ),
					Rings( cell ) ? next : int.MaxValue );

			// Busy fills the gaps with quieter sixteenth chatter.
			if ( cell == 0 && !sparse && noise.Chance( busy ) )
			{
				int sixAt = _time.TickToSample( h.Tick + Timing.TicksPerEighth / 2 );
				if ( _crashRide ) RenderCrashCym( sixAt, _c.CrashVol * kit * 0.22f, _crashDark, true );
				else if ( _ride ) RenderRideCym( sixAt, amp * 0.4f * RideStroke( h.Tick + Timing.TicksPerEighth / 2 - barTick ), _rideBow );
				else RenderHat( sixAt, 0f, amp * 0.4f, noise, _hatTone );
			}
		}

		// The pedal's own part, under a section whose hands are on the ride (see FootOccupancy).
		// The pattern was drawn once for the section; a foot keeps a figure the way a hand does.
		if ( (_ride || _crashRide) && _footCells != 0 )
			for ( int i = 0; i < 8; i++ )
			{
				if ( (_footCells & (1 << i)) == 0 ) continue;
				int t = barTick + i * Timing.TicksPerEighth;
				if ( t >= to ) break;
				RenderHat( _time.TickToSample( t ), 0f, _c.HatVol * KitGain( t, 0.7f, 0.45f ),
					noise, _footTone );
			}

		// THE KICK READS ITS VELOCITY. It used to discard the cell's Vel and take no metric accent
		// and no energy scaling at all, so metal's sixteenth double-kick was the identical
		// waveform at the identical level sixteen times a bar — which is what machine-gunning is.
		// Its depth is under the cymbal hand's and near the fill's: the kick is the floor of the
		// groove and should breathe least.
		var kicks = _kickFig.Slice( barTick, to, _sectionTick, _feel );
		Trace?.Add( TraceVoice.Kick, kicks );
		foreach ( var h in kicks )
			RenderKick( _time.TickToSample( h.Tick ), noise, KitGain( h.Tick, h.Vel, 0.30f ),
				_kickTone, 0f );

		// The kick's own humanising. A groove pattern is exact, and a drummer is not: KICK SYNC is
		// the chance of a stray extra kick pushing into the following beat, rolled per bar so the
		// groove breathes instead of stamping the identical bar out for eight bars running.
		if ( _c.KickSyncChance > 0f )
			for ( int t = barTick + Timing.TicksPerEighth; t < to; t += Timing.TicksPerBeat )
				if ( noise.Chance( _c.KickSyncChance * (0.4f + busy) * _energy ) )
					RenderKick( _time.TickToSample( t ), noise, KitGain( t, 0.85f, 0.30f ),
						_kickTone, 0f );

		foreach ( var h in _snareFig.Slice( barTick, to, _sectionTick, _feel ) )
		{
			bool ghost = h.Value == DrumGroove.Ghost;
			// The groove's own ghost notes thin out with the section rather than hammering a
			// verse as hard as a chorus.
			if ( ghost && !noise.Chance( 0.35f + 0.65f * _energy ) ) continue;
			Trace?.Add( TraceVoice.Snare, h.Tick, !ghost );
			RenderSnare( _time.TickToSample( h.Tick ), noise, ghost );
		}

		// Extra ghosts / toms between the groove's own hits: the "busy" layer. A groove that
		// already fills its own gaps scales this down through GhostRate rather than being
		// special-cased here.
		if ( busy > 0f && !sparse )
			for ( int t = barTick; t < to; t += Timing.TicksPerEighth / 2 )
			{
				if ( !noise.Chance( _c.GhostSnareChance * busy * 0.5f ) ) continue;
				int at = _time.TickToSample( t );
				// The busy layer's toms are the two RACK toms answering each other — the drums a
				// hand can reach without leaving the groove. Which two they are is the kit's, not
				// a pair of frequencies picked here (see TomKit).
				if ( noise.Chance( (1f - _drumTone) * 0.5f ) )
					RenderTom( at, _tomKit, (t / (Timing.TicksPerEighth / 2)) & 1, noise,
						KitGain( t, 0.7f, 0.4f ), TomTone.Default );
				else RenderSnare( at, noise, true );
			}
	}

	/// <summary>
	/// THE FOOT'S OWN PART: how often the pedal closes the hi-hat, per eighth of the bar, while
	/// the hands are on the ride. A riding section used to silence the hi-hat completely, and the
	/// hat is the one voice in the kit that does not need a hand.
	///
	/// MEASURED, off the same source as the grooves and the accent weights (Google Magenta's
	/// Groove MIDI Dataset — see DrumGroove's header). Method: over the 472 4/4 performances,
	/// split by whether the ride carries the pulse (a ride hit at least every four beats), count
	/// note-ons of the PEDAL hi-hat (GM 44) and fold their positions onto one bar at the eighth.
	/// Two things came out of it and only one was expected:
	///
	///   * the foot IS busier when the hands are away — 2.74 pedal hits per bar over 12220 riding
	///     bars, against 1.92 over 8519 bars where the hands are on the hat;
	///   * but it is NOT "2 and 4". Those are the two peaks (16.2% and 17.9% of hits) and the
	///     downbeat is a third (13.1%), while every remaining eighth still carries 9.6–11.6%.
	///     Two chicks a bar on the backbeat is a third of what a drummer's foot actually does,
	///     and it is the part that is easiest to assume you already know.
	///
	/// These are the per-eighth probabilities that reproduce both numbers: each share of the hits
	/// times the 2.74 they are shared out of. The pattern is drawn ONCE PER SECTION from them
	/// rather than rolled per bar, because a drummer's foot keeps a figure the same way a hand
	/// does — the marginals are what a performance averages to, not what it decides every bar.
	/// </summary>
	static readonly float[] FootOccupancy =
		{ 0.36f, 0.26f, 0.44f, 0.32f, 0.31f, 0.28f, 0.49f, 0.28f };

	/// <summary>How hard a ride stroke is, by where it lands. A drummer's "and" is a much lighter
	/// stroke than the beat; the genre's accent weight alone (rock's offbeat is 1.7 dB down) leaves
	/// eight near-equal strokes a bar, and eight near-equal strokes is a WALL however good each one
	/// sounds. Pulling the offbeats back was the single most effective change in the whole cymbal
	/// exercise — more than any edit to the voice itself. Suspect the pattern before the timbre.
	/// </summary>
	static float RideStroke( int tickInBar )
		=> tickInBar % Timing.TicksPerBeat == 0 ? 1f : 0.5f;

	// ── The cymbal hand's cells ──
	// What a cymbal cell means, once it can mean more than "open or not". Openness is a distance
	// and the two foot articulations are not distances at all, which is why the tone and the
	// position are two lookups rather than one.

	static float HatOpenness( int cell ) => cell switch
	{
		DrumGroove.Open => 1f,
		DrumGroove.Half => 0.5f,
		DrumGroove.Splash => 1f,
		_ => 0f,
	};

	HatTone HatFor( int cell ) => cell switch
	{
		DrumGroove.Foot => _footTone,
		DrumGroove.Splash => HatTone.Splash,
		_ => _hatTone,
	};

	/// <summary>Whether this cell leaves the hat RINGING — i.e. whether there is anything for the
	/// next hit's foot to choke. A chick and a splash have already closed the cymbals.</summary>
	static bool Rings( int cell ) => cell == DrumGroove.Open || cell == DrumGroove.Half;

	// ── Fills ──
	// A fill is a span, not "the last beat of the bar". Length is a weighted draw — a beat most
	// of the time, occasionally a whole bar or two — and the long ones are GATED to the
	// boundaries that earn them (into a final chorus, out of a breakdown), because a two-bar
	// fill at every phrase end is not a fill, it is the arrangement.
	//
	// Returns the tick the fill starts at, so the groove above knows where to stop.
	int FillStart( int barTick, int barTicks, bool bigBoundary, Rng rng )
	{
		float r = rng.Next();
		int span;
		if ( r < 0.55f ) span = Timing.TicksPerBeat;                       // one beat
		else if ( r < 0.80f || !bigBoundary ) span = Timing.TicksPerBeat * 2; // two beats (from 3)
		else if ( r < 0.95f ) span = barTicks;                             // a whole bar
		else span = barTicks * 2;                                          // two bars

		// The fill ends on the bar line it is leading into, so a longer one simply starts
		// earlier — two beats start on beat 3, two bars start in the bar before.
		return Math.Max( barTick - barTicks, barTick + barTicks - span );
	}

	static readonly FillShape[] FillShapeTable =
		{ FillShape.Ramp, FillShape.Rolling, FillShape.Pickup, FillShape.Gesture };

	// Occupancy per grid cell within one beat, relative to the beat itself — the measured shape a
	// bar of fill has. The flurry's grid is the same idea at 32nds, with the eighths inside it
	// still carrying the weight, so an acceleration still lands on the beats it passes.
	static readonly float[] FillStraight = { 1f, 0.62f, 1f, 0.62f };
	static readonly float[] FillTriplet = { 1f, 0.62f, 0.62f };
	static readonly float[] FillFlurry = { 1f, 0.5f, 0.62f, 0.5f, 1f, 0.5f, 0.62f, 0.5f };

	/// <summary>How many cells a fill rolls for per beat, whatever grid it is actually on — the
	/// triplet roll, the flurry and the straight sixteenths all pull the same number of values.
	/// A knob (TRIPLET here) must never decide how much of the stream a fill spends.</summary>
	const int FillCells = 8;

	/// <summary>The scale factor that turns a grid's position weights into per-cell probabilities
	/// summing to <paramref name="hitsPerBar"/>.</summary>
	static float FillCellK( float hitsPerBar, float[] grid )
	{
		float perBar = 0f;
		foreach ( var w in grid ) perBar += w;
		return hitsPerBar / (perBar * 4f);
	}

	/// <summary>The per-cell probabilities a density target turns into on a grid.
	///
	/// A WATER-FILL RATHER THAN ONE SCALE FACTOR, and that is the difference between a fill getting
	/// denser and a fill getting louder. The beats reach certainty long before a high target does,
	/// so a flat scale silently drops everything past that point — metal asked for 14 hits a bar
	/// and would have played 13.4 whatever number it wrote down. What a busier drummer actually
	/// adds is ORNAMENT, the "e" and the "a", so the excess goes there. The grid's real ceiling is
	/// four hits a beat, and no genre is near it.</summary>
	static float[] FillChances( float hitsPerBar, float[] grid )
	{
		var p = new float[grid.Length];
		float want = hitsPerBar / 4f;                          // per beat
		float k = FillCellK( hitsPerBar, grid );
		for ( int pass = 0; pass < 6; pass++ )
		{
			float got = 0f, room = 0f;
			for ( int i = 0; i < p.Length; i++ ) { p[i] = Math.Clamp( k * grid[i], 0f, 1f ); got += p[i]; }
			for ( int i = 0; i < p.Length; i++ ) if ( p[i] < 1f ) room += grid[i];
			if ( want - got < 1e-4f || room <= 0f ) break;
			k += (want - got) / room;
		}
		return p;
	}

	/// <summary>What a density target of <paramref name="hitsPerBar"/> actually plays on the
	/// straight grid. The engine suite asserts each genre's target against this: a target the model
	/// cannot reach is a number that quietly buys nothing.</summary>
	internal static float FillDensityOnGrid( float hitsPerBar )
	{
		float hits = 0f;
		foreach ( var p in FillChances( hitsPerBar, FillStraight ) ) hits += p;
		return hits * 4f;
	}

	/// <summary>How dense the fill is at this point in its span, as a multiplier on the genre's
	/// target. <paramref name="u"/> is 0 on the fill's first beat and 1 on its last.</summary>
	static float ShapeDensity( FillShape shape, float u, bool last ) => shape switch
	{
		FillShape.Ramp => 0.45f + 1.1f * u,
		FillShape.Pickup => last ? 1f : 0.22f,
		FillShape.Gesture => 0.15f + 0.35f * u,
		_ => 1f,
	};

	/// <summary>
	/// What the rest of the section is doing where this fill cell lands — the one thing a fill did
	/// not read, on a grid that was already built for it.
	///
	/// A fill is the drummer's bar, but it is not played over silence: the melodic voices play
	/// THROUGH a fill (only the kit hands over), so a fill that puts a hit on the note the tune is
	/// landing on is two things arriving on the same beat. And the seam is what the fill is FOR —
	/// it is crossing one, and leaning on it is the gesture.
	///
	/// A multiplier on a probability, so it changes nothing about how much of the stream a fill
	/// spends: <see cref="FillCells"/>'s rule holds, and the density target still means what it
	/// said. Deliberately gentle in both directions — a fill that dodged the tune outright would
	/// be a fill written by the melody.
	/// </summary>
	float FillAgainst( int tick )
	{
		var sk = _skeleton;
		if ( sk == null ) return 1f;
		int c = sk.CellAt( tick );
		if ( c < 0 ) return 1f;
		return (sk.TuneOn[c] ? 0.6f : 1f) * (sk.Seam[c] ? 1.25f : 1f);
	}

	// One fill across a span. The span is whatever FillStart drew, so the same code plays a
	// one-beat pickup and a two-bar blow-out; the terminal crash lands on the downbeat it is
	// leading into.
	void RenderFill( int fromTick, int toTick, Rng noise, Rng rng )
	{
		int span = toTick - fromTick;
		if ( span <= 0 ) return;
		int beats = Math.Max( 1, span / Timing.TicksPerBeat );

		var shape = rng.PickWeighted( FillShapeTable, _prof.FillShapes );
		// A SHUFFLE IS ALREADY A TRIPLET FEEL, so a fill on the straight grid under one is not
		// straight — it is neither. Ticks are metrical and the shuffle is a warp applied on the way
		// to samples (see Timing), which interpolates between eighth ANCHORS: the four sixteenths of
		// a beat come out 2:2:1:1, so the back half of every beat runs at double the speed of the
		// front. At 115 bpm with swing 0.33 they land at 0/173/347/434 ms. That is right for a comp
		// landing an occasional sixteenth between two eighths the band shares, and wrong for the one
		// voice that runs continuous sixteenths — a drummer shuffling fills in triplets.
		//
		// The Chance draw still happens either way, so the genre's stream position is untouched: the
		// feel decides the GRID, never how much of the stream a fill spends (see FillCells).
		bool triplet = rng.Chance( _c.TripletChance ) || _time.Swing >= GenreProfile.ShuffleGrid;
		bool tomLed = rng.Chance( 0.45f );

		// A fill longer than a bar still has to keep the time while it happens. A gesture or a
		// pickup stretched over two bars is not a sparser fill, it is a hole in the arrangement —
		// the kit has already handed over to it, so there is nothing else playing the beat.
		if ( beats > 4 && shape != FillShape.Rolling ) shape = FillShape.Ramp;

		// And the same rule from the other end: a PICKUP is a wait and then a flurry, so it needs a
		// span to wait in. Over one beat there is nothing to wait through and the shape degenerates
		// into the flurry alone — five or six 32nds in the beat the kit has just handed over to,
		// with no groove either side of them. That is the most common fill length there is (a beat
		// is 55% of the draw), so a genre with any weight on Pickup plays it constantly, and it
		// reads as a drummer arriving late and cramming the whole fill in anyway. A fill that short
		// accelerates into the bar line instead.
		if ( beats == 1 && shape == FillShape.Pickup ) shape = FillShape.Ramp;

		float[] grid = triplet ? FillTriplet : FillStraight;
		// The genre's hits-per-bar turned into per-cell probabilities: the position weights are the
		// SHAPE of a bar's occupancy and this fills them until they sum to the target. The flurry
		// keeps the flat scale instead, because being denser than an ordinary beat is what a flurry
		// IS — water-filling it to the same target would take the acceleration back out of it.
		float[] cells = FillChances( _prof.FillHits, grid );
		float flurryK = FillCellK( _prof.FillHits, grid );

		for ( int b = 0; b < beats; b++ )
		{
			int beatTick = fromTick + b * Timing.TicksPerBeat;
			bool last = b == beats - 1;
			float dens = ShapeDensity( shape, beats == 1 ? 1f : b / (beats - 1f), last );
			// The flurry is a straight-grid gesture: a triplet fill is already a different feel
			// and does not need a second one laid over its last beat.
			bool flurry = shape == FillShape.Pickup && last && !triplet;
			int n = flurry ? FillFlurry.Length : cells.Length;

			for ( int i = 0; i < FillCells; i++ )
			{
				// Both draws happen for every cell of every grid — see FillCells.
				float r = rng.Next(), d = rng.Next();
				if ( i >= n ) continue;
				float p = flurry ? flurryK * FillFlurry[i] : cells[i];
				int cellTick = beatTick + i * Timing.TicksPerBeat / n;
				if ( r >= Math.Clamp( p * dens * FillAgainst( cellTick ), 0f, 1f ) ) continue;

				// A tuplet divides its own span evenly; a straight cell is a grid position and
				// shuffles with everything else the band lands on (see Timing).
				int t = triplet
					? _time.EvenSpan( beatTick, Timing.TicksPerBeat, i / (double)n )
					: _time.TickToSample( cellTick );

				// A fill is a phrase: it leans into the bar line it is landing on, and it reads
				// the genre's accent weights like every other voice in the song.
				float u = (b + i / (float)n) / beats;
				float gain = KitGain( cellTick, 0.72f + 0.33f * u, 0.35f );

				// A fill goes round the kit high to low, which is what a three-piece tom set is
				// laid out for — and it goes there BY INDEX, so a fill cannot reach past the
				// bottom of the kit. It used to sweep six frequencies through a pan map that
				// bottomed out at 145 Hz, so the two lowest drums of every fill shared a position.
				// Snare-led unless the fill is a tom figure; the ride comes in where DrumTone leans
				// high, and the kick is under it either way — "13.2 hits per bar" is the whole kit,
				// and a drummer's foot is part of the kit.
				float tomShare = shape == FillShape.Gesture || tomLed ? 0.55f : 0.26f;
				float rideShare = 0.14f * _drumTone;
				if ( d < tomShare )
					RenderTom( t, _tomKit, Math.Min( TomKit.Count - 1, (int)(u * TomKit.Count) ),
						noise, gain, TomTone.Default );
				else if ( d < tomShare + rideShare )
					RenderRideCym( t, _c.HatVol * gain, _rideBow );
				else if ( d < tomShare + rideShare + 0.08f )
					RenderKick( t, noise, gain, _kickTone, 0f );
				else RenderSnare( t, noise, false, gain );
			}
		}
		// The fill lands on the downbeat it was leading into, and it is a crash at a LEVEL now:
		// the voice had no gain parameter at all, so the loudest thing in a song arrived at full
		// scale whatever the section's energy, the velocity or the genre's accent said.
		bool darkCrash = rng.Chance( 0.4f );
		RenderCrashCym( _time.TickToSample( toTick ), _c.CrashVol * KitGain( toTick, 1f, 0.35f ),
			darkCrash ? _crashDark : _crashBright, darkCrash );
	}
}
gamah.skafinity / Code/Engine/Master.cs
Game library
using System;
using System.Collections.Generic;

namespace Skafinity;

// Master bus: the reverb, then soft-clip and normalize.
//
// Part of the MusicGen engine — see MusicGen.cs.

public sealed partial class MusicGen
{
	// Master: gentle soft-clip + normalize. The mix peak is first normalized to 1.0
	// BEFORE the soft-clipper so it always has headroom — otherwise a hot sustained
	// bed (all voices flat at 1.0) saturated the tanh and swallowed the drum
	// transients (kick/snare washed out). MasterDrive now sets how hard a
	// peak-normalized signal hits the clipper, so the dynamics stay intact.
	// Call only after every RenderPitchedRange window has completed.
	float Master()
	{
		int total = _bufL.Length;
		float rawPeak = 0f;
		for ( int i = 0; i < total; i++ )
			rawPeak = Math.Max( rawPeak, Math.Max( MathF.Abs( _bufL[i] ), MathF.Abs( _bufR[i] ) ) );
		float pre = rawPeak > 0.0001f ? _c.MasterDrive / rawPeak : _c.MasterDrive;

		for ( int i = 0; i < total; i++ )
		{
			_bufL[i] = (float)Math.Tanh( _bufL[i] * pre );
			_bufR[i] = (float)Math.Tanh( _bufR[i] * pre );
		}

		// A touch of stereo room reverb — the dry mix alone read flat/"16-bit".
		ApplyReverb();

		float peak = 0f;
		for ( int i = 0; i < total; i++ )
		{
			float a = Math.Max( MathF.Abs( _bufL[i] ), MathF.Abs( _bufR[i] ) );
			if ( a > peak ) peak = a;
		}
		return peak > 0.0001f ? _c.MasterPeak / peak : 1f;
	}

	// The song's room, from the two Config knobs that shape it — trimmed by the genre's own mix
	// profile. A room is part of what a genre sounds like: ska is recorded in one, metal is
	// recorded dry and close, country sits in between and centred. The REVERB knob still rides on
	// top; the trim moves what "0.5" means for this genre.
	void ApplyReverb()
	{
		float wet = Math.Clamp( _reverbWet * _c.MasterReverb * MixTrim( _prof.Mix.Reverb ), 0f, 1f );
		if ( wet <= 0.0001f ) return;
		// Tail length. The top of this used to be 0.98, which over combs 25–34 ms long is a ~9
		// second decay — long past a room and into a delay line whose repeats smear every chord
		// into the next. 0.94 is ~3 s, which is the longest tail that is still a SPACE the band is
		// playing in. The tail also darkens as it lengthens: a bright decay that outlasts the bar
		// accumulates top end instead of dying, and that ringing IS the muddiness.
		float decay = Math.Clamp( _c.ReverbDecay, 0f, 1f );
		float feedback = 0.70f + 0.24f * decay;
		float damp = 0.25f + 0.35f * decay;
		Reverb.Process( _bufL, 0, wet, feedback, damp, _sr );
		Reverb.Process( _bufR, Reverb.StereoSpread, wet, feedback, damp, _sr );
	}
}
gamah.skafinity / Code/Engine/Wav.cs
Game library
using System;
using System.Collections.Generic;

namespace Skafinity;

/// <summary>
/// The WAV container — 16-bit PCM, the one format both targets can hand straight to a player.
/// Stateless: it wraps samples somebody else rendered.
/// </summary>
static class Wav
{
	/// <summary>Clamp a −1..1 mix sample to signed 16-bit.</summary>
	public static short ToS16( float v ) => (short)(Math.Clamp( v, -1f, 1f ) * 32767f);

	/// <summary>Wrap already-rendered 16-bit samples in a WAV. Mono or interleaved stereo per
	/// <paramref name="channels"/>.</summary>
	public static byte[] FromSamples( short[] samples, int channels, int sampleRate )
	{
		int dataSize = samples.Length * 2;
		int blockAlign = channels * 2;
		var bytes = new List<byte>( 44 + dataSize );
		void Str( string s ) { foreach ( var ch in s ) bytes.Add( (byte)ch ); }
		void U32( uint v ) { bytes.Add( (byte)v ); bytes.Add( (byte)(v >> 8) ); bytes.Add( (byte)(v >> 16) ); bytes.Add( (byte)(v >> 24) ); }
		void U16( ushort v ) { bytes.Add( (byte)v ); bytes.Add( (byte)(v >> 8) ); }
		Str( "RIFF" ); U32( (uint)(36 + dataSize) ); Str( "WAVE" );
		Str( "fmt " ); U32( 16 ); U16( 1 ); U16( (ushort)channels );
		U32( (uint)sampleRate ); U32( (uint)(sampleRate * blockAlign) ); U16( (ushort)blockAlign ); U16( 16 );
		Str( "data" ); U32( (uint)dataSize );
		foreach ( var s in samples ) { ushort u = (ushort)s; bytes.Add( (byte)u ); bytes.Add( (byte)(u >> 8) ); }
		return bytes.ToArray();
	}
}

public sealed partial class MusicGen
{
	// ── Output ──
	short[] ToShorts( float gain )
	{
		int n = _bufL.Length;
		var s = new short[n * Channels];
		for ( int i = 0; i < n; i++ )
		{
			s[i * 2] = Wav.ToS16( _bufL[i] * gain );
			s[i * 2 + 1] = Wav.ToS16( _bufR[i] * gain );
		}
		return s;
	}

	/// <summary>Wrap already-rendered 16-bit samples in a WAV (for export). Mono or
	/// interleaved stereo per <paramref name="channels"/>.</summary>
	public static byte[] WavFromSamples( short[] samples, int channels, int sampleRate )
		=> Wav.FromSamples( samples, channels, sampleRate );

	byte[] EncodeWav( float gain ) => Wav.FromSamples( ToShorts( gain ), Channels, _sr );
}
gamah.skafinity / Code/SkafinityCommands.cs
Game library
using System.Linq;
using Sandbox;

namespace Skafinity;

/// <summary>
/// Console commands for driving the player and the panel from inside the editor.
///
/// <para>They exist because the two things a host most needs to try are the two things it cannot
/// reach without writing code first. The board <b>ships no launcher</b> — visibility is
/// host-driven on purpose, so it imposes nothing on your HUD — which means a freshly-dropped
/// <see cref="SkafinityMusicPanel"/> renders nothing at all until you have bound
/// <see cref="SkafinityMusicPanel.IsOpen"/> to something. And <see cref="SkafinityTheme.Accent"/>
/// is a static a game sets at startup, so seeing what your colour looks like used to mean a
/// rebuild per guess. <c>skafinity_panel</c> and <c>skafinity_theme</c> are those two, live.</para>
///
/// <para>The rest are the seed: play one, step it, switch genre, reroll, and read back either the
/// player's state or what the composer actually decided.</para>
///
/// <para>s&amp;box-only (outside <c>Code/Engine/</c>), and client-side — the player is client-only
/// (<c>DontExecuteOnServer</c>), so these are too.</para>
/// </summary>
public static class SkafinityCommands
{
	/// <summary>Name of the GameObject <see cref="Spawn"/> builds. Also how <see cref="Despawn"/>
	/// finds it again, so it only ever destroys its own rig and never a scene-authored player.</summary>
	const string RigName = "Skafinity (console)";

	// Every command needs the player, and "there isn't one" is the single most likely reason a
	// command does nothing — so say so rather than failing silently.
	static SkafinityPlayer Player()
	{
		var scene = Sandbox.Game.ActiveScene;
		if ( scene == null ) { Log.Warning( "[Skafinity] no active scene." ); return null; }

		var p = scene.GetAllComponents<SkafinityPlayer>().FirstOrDefault();
		if ( p == null )
			Log.Warning( "[Skafinity] no SkafinityPlayer in the scene — run skafinity_spawn for a "
				+ "throwaway one, or add the component to a GameObject yourself." );
		return p;
	}

	/// <summary>Build a throwaway board (and a player, if the scene hasn't got one) on a runtime
	/// GameObject, so the library can be tried in ANY scene without one being authored for it.
	/// <c>skafinity_despawn</c> removes it.</summary>
	/// <remarks>It never makes a SECOND of anything: a board already in the scene — a previous rig
	/// or the game's own UI — is the one it hands back, and a player already in the scene is the
	/// one the board drives. So this is safe to run in a game that has Skafinity wired up properly,
	/// where it does nothing but tell you so.</remarks>
	/// <remarks>Client-local and never saved: <c>NetworkMode.Never</c> so it is not replicated,
	/// <c>GameObjectFlags.NotSaved</c> so it cannot end up committed in someone's scene file. It
	/// carries its OWN <c>ScreenPanel</c> rather than hunting for the scene's, which is what makes
	/// it work in a scene that has no UI root at all. Shape copied from rotaliate-client's
	/// <c>LocalMusicSystem</c>, which builds the same three components for real.</remarks>
	[ConCmd( "skafinity_spawn" )]
	public static void Spawn()
	{
		var panel = BuildRig( out bool created );
		if ( panel == null ) return;

		Log.Info( created
			? $"[Skafinity] rig ready — '{RigName}'. skafinity_panel opens it, skafinity_despawn removes it."
			: $"[Skafinity] nothing spawned — this scene already has a board, on '{panel.GameObject?.Name}'. "
			  + "skafinity_panel opens it." );
	}

	/// <summary>Destroy the rig <c>skafinity_spawn</c> built. Leaves a scene-authored player alone —
	/// it only removes the GameObject it made.</summary>
	[ConCmd( "skafinity_despawn" )]
	public static void Despawn()
	{
		var scene = Sandbox.Game.ActiveScene;
		if ( scene == null ) { Log.Warning( "[Skafinity] no active scene." ); return; }

		var rig = FindRig( scene );
		if ( rig == null ) { Log.Info( "[Skafinity] no console rig to remove." ); return; }

		rig.Destroy();
		Log.Info( $"[Skafinity] removed '{RigName}'." );
	}

	/// <summary>Open/close the settings board — the panel ships no launcher of its own, so this is
	/// how you see it at all. Builds a rig first if the scene has no board (see
	/// <see cref="Spawn"/>), so this one command works in an empty scene.</summary>
	[ConCmd( "skafinity_panel" )]
	public static void TogglePanel()
	{
		// Nothing to toggle rather than nothing to do: the point of the command is to see the board,
		// and needing a scene authored first is the whole problem it exists to solve. BuildRig
		// returns the scene's own board where there is one, so this never makes a second.
		var panel = BuildRig( out bool created );
		if ( panel == null ) return;
		if ( created )
			Log.Info( $"[Skafinity] no board in the scene — built '{RigName}'. skafinity_despawn removes it." );

		panel.Toggle();
		Log.Info( $"[Skafinity] board {( panel.IsOpen ? "OPEN" : "closed" )}." );
	}

	// The rig itself. Returns the board to drive — an existing one wherever there is one — or null
	// with a reason logged. `created` says whether anything was actually built.
	static SkafinityMusicPanel BuildRig( out bool created )
	{
		created = false;
		var scene = Sandbox.Game.ActiveScene;
		if ( scene == null ) { Log.Warning( "[Skafinity] no active scene." ); return null; }

		// A play-mode thing. In the editor's edit scene there is no audio to hear and no reason to
		// be adding objects to what someone is authoring, even unsaveable ones.
		if ( scene.IsEditor )
		{
			Log.Warning( "[Skafinity] press play first — the rig is built into the running scene, not the edit scene." );
			return null;
		}

		// A headless server has no audio device and no screen; the player is DontExecuteOnServer
		// for the same reason, so building it there would produce an inert object and a puzzle.
		if ( Application.IsDedicatedServer )
		{
			Log.Warning( "[Skafinity] not on a dedicated server — Skafinity is client-side (audio + UI)." );
			return null;
		}

		// NEVER a second board. Any panel already in the scene is the one to drive, whether it is a
		// previous rig or one the game authored — two boards would be two sets of controls over one
		// player, and the second would be the game's own UI duplicated by a debug command.
		var existing = scene.GetAllComponents<SkafinityMusicPanel>().FirstOrDefault();
		if ( existing != null ) return existing;

		created = true;
		var go = new GameObject( true, RigName ) { Flags = GameObjectFlags.NotSaved };
		go.NetworkMode = NetworkMode.Never;   // strictly local: this is a test rig, not game state

		// Its own UI root, so this works in a scene with no ScreenPanel of its own.
		go.Components.Create<ScreenPanel>();

		// Reuse a player the scene already has rather than starting a second soundtrack over it.
		var player = scene.GetAllComponents<SkafinityPlayer>().FirstOrDefault()
			?? go.Components.Create<SkafinityPlayer>();

		var panel = go.Components.Create<SkafinityMusicPanel>();
		panel.Player = player;
		return panel;
	}

	// Only ever OUR GameObject: matched by name, so a scene-authored player is never a candidate.
	static GameObject FindRig( Scene scene )
	{
		foreach ( var p in scene.GetAllComponents<SkafinityMusicPanel>() )
			if ( p.GameObject != null && p.GameObject.Name == RigName )
				return p.GameObject;
		return null;
	}

	/// <summary>Retint the board from one colour: <c>skafinity_theme #ff8a3d</c>. Pass
	/// <c>clear</c> (or <c>none</c> / <c>neutral</c>) to go back to the neutral gray/black default.
	/// This is the whole of what a consuming game does — it sets
	/// <see cref="SkafinityTheme.Accent"/> once — so what you see here is what you get by shipping
	/// that one line.</summary>
	[ConCmd( "skafinity_theme" )]
	public static void SetTheme( string accent )
	{
		if ( string.IsNullOrWhiteSpace( accent ) || accent is "clear" or "none" or "neutral" )
		{
			SkafinityTheme.Accent = null;
			Log.Info( "[Skafinity] theme cleared — neutral gray/black (the library default)." );
			return;
		}

		var c = Color.Parse( accent );
		if ( c == null )
		{
			Log.Warning( $"[Skafinity] couldn't parse '{accent}' as a colour — try a hex like #2f9450." );
			return;
		}

		SkafinityTheme.Accent = c;
		Log.Info( $"[Skafinity] accent = {accent}. In your game: SkafinityTheme.Accent = Color.Parse( \"{accent}\" );" );
	}

	/// <summary>Play a seed: <c>tag:n[:genre][:vibe]</c> (a bare <c>tag</c> is song 0). Pass
	/// <c>default</c> to go back to the default tag and vibe.</summary>
	[ConCmd( "skafinity_seed" )]
	public static void PlaySeed( string seed )
	{
		var p = Player();
		if ( p == null ) return;

		if ( seed is "default" ) { p.SetTag( "" ); Log.Info( "[Skafinity] back to the default tag and vibe." ); return; }

		p.PlaySeed( seed );
		Log.Info( $"[Skafinity] playing {p.CurrentSeed}" );
	}

	/// <summary>Next song in the sequence.</summary>
	[ConCmd( "skafinity_next" )]
	public static void Next() { var p = Player(); if ( p == null ) return; p.NextSong(); Log.Info( $"[Skafinity] → {p.CurrentSeed}" ); }

	/// <summary>Previous song — replays the exact earlier song, not a fresh one.</summary>
	[ConCmd( "skafinity_prev" )]
	public static void Prev() { var p = Player(); if ( p == null ) return; p.PrevSong(); Log.Info( $"[Skafinity] ← {p.CurrentSeed}" ); }

	/// <summary>Switch genre by index. Run it with a junk index to print the roster.</summary>
	[ConCmd( "skafinity_genre" )]
	public static void SetGenre( int genre )
	{
		var p = Player();
		if ( p == null ) return;

		if ( genre < 0 || genre >= VibeCodec.GenreCount )
		{
			Log.Warning( $"[Skafinity] genre {genre} is out of range. {Roster()}" );
			return;
		}

		p.SetGenre( genre );
		Log.Info( $"[Skafinity] genre {genre} = {VibeCodec.Genres[genre]} — {p.CurrentSeed}" );
	}

	/// <summary>Reroll the vibe: every knob thrown somewhere new and PINNED there, keeping the genre
	/// and your per-instrument volumes. <c>skafinity_station</c> is the other die — a different song
	/// rather than a different taste.</summary>
	[ConCmd( "skafinity_reroll" )]
	public static void Reroll()
	{
		var p = Player();
		if ( p == null ) return;

		p.RerollVibe();
		Log.Info( $"[Skafinity] rerolled — {p.CurrentSeed}" );
	}

	/// <summary>A fresh random station at song 0. Anything pinned stays pinned.</summary>
	[ConCmd( "skafinity_station" )]
	public static void Station()
	{
		var p = Player();
		if ( p == null ) return;

		p.RerollStation();
		Log.Info( $"[Skafinity] new station — {p.StationSeed}" );
	}

	/// <summary>Flip shuffle: on, every next song is a whole new station rather than the next song of
	/// this one.</summary>
	[ConCmd( "skafinity_shuffle" )]
	public static void ToggleShuffle()
	{
		var p = Player();
		if ( p == null ) return;

		p.SetShuffle( !p.Shuffle );
		Log.Info( $"[Skafinity] shuffle {( p.Shuffle ? "on" : "off" )} — {p.StationSeed}" );
	}

	/// <summary>Pause or resume, keeping the place in the song.</summary>
	[ConCmd( "skafinity_pause" )]
	public static void TogglePause()
	{
		var p = Player();
		if ( p == null ) return;

		p.TogglePlay();
		var at = p.Playhead();
		Log.Info( $"[Skafinity] {( p.IsPaused ? "paused" : "playing" )} at {SkafinityBoard.Time( at.Time, at.Duration > 0 )}" );
	}

	/// <summary>Write the playing song to a .wav under the s&amp;box data folder.</summary>
	[ConCmd( "skafinity_save" )]
	public static void Save()
	{
		var p = Player();
		if ( p == null ) return;

		var name = p.SaveCurrentToFile();
		Log.Info( string.IsNullOrEmpty( name )
			? "[Skafinity] couldn't save — nothing rendered yet?"
			: $"[Skafinity] saved {name} to your s&box data folder." );
	}

	/// <summary>What the player is doing right now: the seed, the transport, and whether the
	/// shared house mix actually loaded.</summary>
	[ConCmd( "skafinity_status" )]
	public static void Status()
	{
		var p = Player();
		if ( p == null ) return;

		int genre = p.EffectiveConfig()?.Genre ?? 0;
		var at = p.Playhead();
		Log.Info( "── skafinity_status ──" );
		Log.Info( $"   seed      {p.CurrentSeed}   (n {p.N}, genre {genre} = {VibeCodec.Genres[genre]})" );
		Log.Info( $"   station   {p.StationSeed}   (position {p.Position} on the line)" );
		Log.Info( $"   transport {( p.Enabled ? "on" : "MUTED" )}, vol {p.Volume:0.00}, "
			+ $"{( p.IsPaused ? "paused" : p.IsPlaying ? "playing" : "not playing" )} "
			+ $"{SkafinityBoard.Time( at.Time, at.Duration > 0 )} / {SkafinityBoard.Time( at.Duration, at.Duration > 0 )}"
			+ $"{( p.IsBuffering ? ", BUFFERING" : p.IsGenerating ? ", generating ahead" : "" )}" );
		Log.Info( $"   rolling   genre {( p.GenrePinned ? "pinned" : "per song" )}, "
			+ $"vibe {( p.VibePinned ? "pinned" : "per song" )}" );
		Log.Info( $"   shuffle   {( p.Shuffle ? "on — every next song is a new station" : "off — walking this station" )}" );
		Log.Info( $"   output    {p.SampleRate} Hz, {p.RenderThreads} render thread(s)" );
		// Zero here is the interesting case: the baseline mix is then the engine's compiled
		// defaults, not the file the web toy reads, and nothing else would ever say so.
		Log.Info( p.HouseConfigCount > 0
			? $"   housemix  {p.HouseConfigCount} values from skafinity.config.json"
			: "   housemix  NOT LOADED — skafinity.config.json isn't mounted, so the baseline mix is "
			  + "the compiled defaults rather than the shared file. Check it shipped with the addon." );
		Log.Info( $"   theme     {( SkafinityTheme.Accent == null ? "neutral (accent unset)" : SkafinityTheme.Accent.ToString() )}" );
		Log.Info( $"   {Roster()}" );
	}

	/// <summary>What the composer decided for the song playing: tempo, swing, key, changes,
	/// voicing, groove, part and tune lengths, ending, and the form. This is the "why does this
	/// seed sound wrong" tool — reading the decisions beats inferring them from the audio.
	/// Re-plans the song, so expect a short hitch.</summary>
	[ConCmd( "skafinity_explain" )]
	public static void Explain()
	{
		var p = Player();
		if ( p == null ) return;

		Log.Info( $"── skafinity_explain {p.CurrentSeed} ──" );
		Log.Info( p.ExplainCurrent() );
	}

	static string Roster() =>
		"genres: " + string.Join( "  ", VibeCodec.Genres.Select( ( g, i ) => $"{i}={g}" ) );
}
gamah.skafinity / Engine/Harmony.cs
Game library
using System;
using System.Collections.Generic;

namespace Skafinity;

/// <summary>
/// Harmony: the per-genre scale / progression / bass-pattern tables, and the degree→pitch
/// maths that reads them.
///
/// A progression entry is a SCALE DEGREE, not a semitone — so the same progression table
/// reads as major or minor depending on the scale drawn alongside it, and a degree of 5
/// against a minor scale is a ♭VI. Degrees are unbounded: <see cref="ScaleMidi"/> wraps
/// octaves rather than clamping, so running off either end of a scale still lands on a sane
/// pitch. That is what lets a progression be any length — nothing here assumes four.
///
/// Stateless by design: every entry point takes the scale it should read. MusicGen keeps thin
/// instance wrappers that supply the song's own scale. Which table a genre draws from is
/// <see cref="GenreProfile"/>'s business, not this file's — these are just the tables.
///
/// NO TWO GENRES SHARE MORE THAN ONE PROGRESSION, OR MORE THAN ONE SCALE. Sharing them is how
/// six genres came to draw byte-identical changes (I–V–vi–IV was in four of them; major was in
/// four scale tables), so both sets of tables are pruned to keep the genres apart and the engine
/// test asserts the cap on each. Adding an entry means checking it against the other five.
///
/// WEIGHTS ARE REAL WEIGHTS. The tables used to bias a draw by listing an entry twice, which
/// silently made "how likely" and "how many entries" the same knob — a genre could not lean on
/// its home mode without also diluting the overlap cap. Each table now carries a parallel weight
/// array (see <see cref="GenreProfile"/>), drawn with one <c>rng.Next()</c> exactly like the old
/// <c>Pick</c>, so a genre's draw count never depends on how it is weighted.
/// </summary>
static class Harmony
{
	/// <summary>Bass-pattern cell: no onset (the previous note sustains).</summary>
	public const int Rest = -99;

	/// <summary>Bass-pattern cell: walk into the next chord instead of playing a fixed
	/// offset.</summary>
	public const int Approach = 99;

	// ── Chord voicings ──
	// Offsets in SCALE-DEGREE space from the chord's own degree, so a voicing follows the mode
	// the way the rest of the engine does: {0,2,4} is a diatonic triad, {0,2,4,6} adds the 7th,
	// {0,4} is the bare power chord (root + 5th). Nothing in the engine played anything but a
	// triad or a power chord, which is why every genre's harmony read as the same primary-colour
	// chord set even under different roots. The chordal voices draw from their genre's table.
	/// <summary>Degree offsets inside a voicing. The THIRD decides major or minor and is the note a
	/// driven guitar leaves out; the FOURTH and the FIFTH are the ones that must stay PERFECT (see
	/// MusicGen.VoicedTone), because they are what "sus4" and "power chord" mean.</summary>
	public const int Third = 2, Fourth = 3, Fifth = 4;

	/// <summary>The SECOND — the other degree a suspension puts where the third belongs.</summary>
	public const int Second = 1;

	/// <summary>Index of the voice a suspension occupies in <paramref name="voicing"/>, or -1 if it
	/// is not suspended.
	///
	/// A SUSPENSION IS A DELAYED THIRD, NOT A CHORD QUALITY. sus4 and sus2 put the fourth or the
	/// second in the third's place, so a chord voiced that way states no quality — and the song's
	/// voicing is drawn once, for every chordal voice and every chord. Held that way for a whole
	/// song nothing is out of key and every voice agrees; the song simply has no major and no
	/// minor, and an ear with nothing to resolve to hears the ambiguity as dissonance. The
	/// suspended note has to arrive somewhere, so <see cref="MusicGen.VoicingAt"/> hands the
	/// chordal voices the resolved spelling over the back half of every chord's span: the chord
	/// hangs, then it lands.
	///
	/// A voicing that already contains the third is not suspended — the sixth's added 6th and the
	/// add9's 9th are colour over a stated triad, not a substitution. Neither is the power chord:
	/// it OMITS the third rather than replacing it, which is a sound in its own right (it is what
	/// a driven guitar plays) and there is nothing owed.</summary>
	public static int SuspendedVoice( int[] voicing )
	{
		int sus = -1;
		for ( int i = 0; i < voicing.Length; i++ )
		{
			if ( voicing[i] == Third ) return -1;
			if ( voicing[i] == Second || voicing[i] == Fourth ) sus = i;
		}
		return sus;
	}

	public static readonly int[] Triad = { 0, 2, 4 };
	public static readonly int[] Seventh = { 0, 2, 4, 6 };
	public static readonly int[] Ninth = { 0, 2, 4, 6, 8 };
	public static readonly int[] Sixth = { 0, 2, 4, 5 };
	public static readonly int[] Sus4 = { 0, 3, 4 };
	public static readonly int[] Sus2 = { 0, 1, 4 };
	public static readonly int[] Add9 = { 0, 2, 4, 8 };
	public static readonly int[] Power = { 0, 4 };
	public static readonly int[] PowerFlat7 = { 0, 4, 6 };

	// ── Ska-punk harmony (Genre 0) ──
	// Bright and major: third-wave ska is upbeat major-key music, and mixolydian is what keeps its
	// ♭VII moves available. The wave shift moved the WEIGHT toward plain major rather than the
	// table itself — the modes were already right, which is the part of the genre that did not
	// need retuning.
	public static readonly int[][] SkaPunkScales =
	{
		new[] { 0, 2, 4, 5, 7, 9, 11 }, // major
		new[] { 0, 2, 4, 5, 7, 9, 10 }, // mixolydian
	};
	public static readonly int[] SkaPunkScaleWeights = { 4, 2 };

	// The 7ths and 9ths stay: they are what make the CLEAN SKANK read as ska rather than as a
	// bright rock stab, and the skank is still what the verses play. They cost nothing in the loud
	// sections — the driven guitar drops its third there anyway (see DrivenVoicing), so the same
	// voicing spells a rocksteady chop in the verse and a power chord in the chorus.
	public static readonly int[][] SkaPunkVoicings = { Seventh, Ninth, Triad, Sixth };
	public static readonly int[] SkaPunkVoicingWeights = { 3, 2, 3, 1 };

	// The bright turnarounds ska keeps. The mixolydian ♭VII moves are the ones doing the work
	// here: punk and pop hold the plain-major anthem loops (I–V–vi–IV, vi–IV–I–V, I–IV–V–V,
	// I–V–IV–V), and ska-punk sits close enough to punk that ♭VII is most of what is left to tell
	// them apart harmonically — so nothing here may drift toward those tables. The 50s/rocksteady
	// ii–V turnaround went with the wave: it is the sound of the era this genre no longer is.
	public static readonly int[][] SkaPunkProgressions =
	{
		new[] { 0, 3, 4, 3 }, // I–IV–V–IV
		new[] { 0, 6, 3, 0 }, // I–♭VII–IV–I (mixolydian)
		new[] { 0, 6, 4, 0 }, // I–♭VII–V–I (the mixolydian cadence)
		new[] { 0, 3, 0, 4 }, // I–IV–I–V
		new[] { 5, 4, 3, 4 }, // vi–V–IV–V (the minor-tinged vamp)
		new[] { 0, 0, 3, 4 }, // I pedal → IV–V
	};

	// ── Bass pattern libraries ──
	// Cells are semitone offsets from the chord root, one per eighth; Rest carries no onset (the
	// previous note sustains through it) and Approach walks into the next chord.
	//
	// These are Patterns, not int[8]: a pattern owns its LENGTH, so a genre's line can be a
	// two-bar phrase that answers itself or a four-bar one that varies its last bar, instead of
	// one bar repeated until the section ends. That is also what keeps the libraries apart — the
	// old tables shared literal rows ({0,0,0,0,0,0,0,App} was in four of the five).
	static Pattern P( params int[] cells ) => Pattern.Eighths( cells );

	// Ska-punk: a DRIVING bass. The spacious one-drop and the long legato 1↔5 lines that
	// used to live here are rocksteady/reggae playing — the right part for a genre this engine does
	// not have yet (see PLAN.md), and recoverable from git history when it does. A ska-punk bassist
	// runs eighths under the skank and pops the octave, closer to punk than to reggae; what keeps
	// these apart from PunkBass is that they still MOVE — walking lines and octave answers rather
	// than the undifferentiated chug that is punk's whole idea.
	public static readonly Pattern[] SkaPunkBass =
	{
		P( 0, 0, 0, 0, 7, 0, 0, 0,
		   0, 0, 0, 0, 12, 0, 7, Approach ),                              // driving eighths, octave on the way out (2 bars)
		P( 0, 12, 0, 12, 0, 12, 7, Approach ),                            // the octave pump (1 bar)
		P( 0, 0, 2, 0, 4, 0, 5, 0,
		   7, 0, 5, 0, 4, 0, 2, Approach ),                               // walking eighths up and back (2 bars)
		P( 0, Rest, 0, 7, Rest, 0, 12, Rest,
		   0, Rest, 0, 7, Rest, 12, 7, Approach ),                        // the verse line that breathes under the skank (2 bars)
		P( 0, 0, 0, 0, 0, 0, 12, 0,
		   0, 0, 0, 0, 7, 0, 12, 0,
		   0, 0, 0, 0, 0, 0, 12, 0,
		   0, 7, 5, 4, 2, 0, 0, Approach ),                               // four bars that walk out of the phrase
	};

	// ── Rock harmony (Genre 1) ──
	// Minor-3rd modes throughout: the RockProgressions are written as MINOR (i–♭VII–♭VI …), so a
	// major-3rd mode would flip the tonic major and the dark rock vamp would evaporate. Phrygian
	// went to metal — rock and metal sharing three modes was how two genres in the same tonality
	// could draw the identical mode under their now-different changes.
	public static readonly int[][] RockScales =
	{
		new[] { 0, 2, 3, 5, 7, 8, 10 }, // natural minor (aeolian)
		new[] { 0, 2, 3, 5, 7, 9, 10 }, // dorian (minor, brighter ♮6 — classic rock)
	};
	public static readonly int[] RockScaleWeights = { 3, 2 };

	// Rock voices the triad plainly and reaches for a suspension rather than an extension — the
	// sus4 that hangs and resolves is the rock chord move ska's 7ths and 9ths are not.
	public static readonly int[][] RockVoicings = { Triad, Sus4, Power };
	public static readonly int[] RockVoicingWeights = { 3, 2, 2 };

	// Degrees are read against the (often minor) scale, so 5 = ♭VI, 6 = ♭VII, 3 = iv, 4 = v,
	// 2 = ♭III. Rock and metal both live in minor, so they had three progressions in common and
	// could draw the identical vamp; the ♭VI/♭II-leaning ones are metal's now and rock keeps the
	// ♭VII-driven ones.
	public static readonly int[][] RockProgressions =
	{
		new[] { 0, 5, 6, 0 }, // i–♭VI–♭VII–i
		new[] { 0, 3, 6, 0 }, // i–iv–♭VII–i
		new[] { 0, 0, 6, 6 }, // i / ♭VII riff vamp
		new[] { 0, 6, 0, 3 }, // i–♭VII–i–iv
		new[] { 0, 6, 3, 4 }, // i–♭VII–iv–v
		new[] { 0, 2, 3, 6 }, // i–♭III–iv–♭VII
	};

	// Rock: the engine room. Root-driven and locked to the kick, but phrased over two bars so it
	// pushes and releases rather than chugging identically forever.
	public static readonly Pattern[] RockBass =
	{
		P( 0, Rest, 0, 0, Rest, 0, 0, Rest,
		   0, Rest, 0, 0, Rest, 0, 12, Approach ),                        // syncopated driver (2 bars)
		P( 0, Rest, Rest, 0, Rest, Rest, 0, Rest,
		   0, Rest, Rest, 0, Rest, 12, 7, Approach ),                     // dotted push (2 bars)
		P( 0, 0, 12, 0, 0, 0, 12, Rest,
		   0, 0, 12, 0, 7, 5, 3, Approach ),                              // octave pushes → walkdown (2 bars)
		P( 0, Rest, 0, Rest, 0, Rest, 0, Approach ),                      // quarter pulse (1 bar)
	};

	// ── Country harmony (Genre 2) ──
	// Country is the plainest major genre and stays that way: one mode, and the variety comes from
	// the changes and the boom-chick underneath. Mixolydian is ska's — country and ska sharing
	// both bright modes is exactly the near-duplication the cap exists to stop, and a second mode
	// bought country nothing its progressions do not already give it.
	public static readonly int[][] CountryScales =
	{
		new[] { 0, 2, 4, 5, 7, 9, 11 }, // major
	};
	public static readonly int[] CountryScaleWeights = { 1 };

	// Country's colour chords are the 6th and the sus4 — the open, ringing shapes a Telecaster
	// plays. No 7ths: that is ska's sound, and a dominant 7th everywhere reads as blues.
	public static readonly int[][] CountryVoicings = { Triad, Sixth, Sus4 };
	public static readonly int[] CountryVoicingWeights = { 3, 2, 1 };

	// Country is the plainest of the major genres — I, IV and V and not much else — so it keeps
	// the backbone and the two-chord vamps, and the anthem loops go to punk/pop.
	public static readonly int[][] CountryProgressions =
	{
		new[] { 0, 3, 4, 0 }, // I–IV–V–I (the country backbone)
		new[] { 0, 0, 4, 4 }, // I–V vamp
		new[] { 0, 4, 3, 0 }, // I–V–IV–I
		new[] { 0, 3, 0, 3 }, // I–IV two-chord
		new[] { 0, 4, 0, 4 }, // I–V two-chord
	};

	// Country: "boom-chick" — the bass alternates root and fifth on the beats while the guitar and
	// snare take the off "chick", and it walks in step to the next chord rather than pushing.
	// Two-bar phrases so the walkdown has somewhere to happen.
	public static readonly Pattern[] CountryBass =
	{
		P( 0, Rest, 7, Rest, 0, Rest, 7, Rest,
		   0, Rest, 7, Rest, 0, Rest, 5, Approach ),                      // alternating root–fifth (2 bars)
		P( 0, Rest, 7, Rest, 12, Rest, 7, Rest,
		   0, Rest, 7, Rest, 9, 7, 5, Approach ),                         // with the octave, walks out (2 bars)
		P( 0, Rest, 7, Rest, 0, Rest, 7, Rest,
		   0, Rest, 7, Rest, 4, 5, 7, Approach ),                         // scalar walkup (2 bars)
		P( 0, Rest, 7, Rest, 0, Rest, 7, Approach ),                      // the plain boom-chick (1 bar)
	};

	// ── Metal harmony (Genre 3) ──
	// Phrygian is the metal mode and carries the weight here; harmonic minor is the neoclassical
	// colour. Aeolian stays as the common ground with rock — one shared mode is honest, three was
	// two genres playing the same thing in a different tempo band.
	public static readonly int[][] MetalScales =
	{
		new[] { 0, 1, 3, 5, 7, 8, 10 }, // phrygian (the metal mode)
		new[] { 0, 2, 3, 5, 7, 8, 10 }, // natural minor (aeolian)
		new[] { 0, 2, 3, 5, 7, 8, 11 }, // harmonic minor
	};
	public static readonly int[] MetalScaleWeights = { 4, 2, 1 };

	// Metal is correctly 3rd-less: the power chord, and the ♭7 on top of it for the wider riff
	// voicing. A major or minor triad through that much gain is mud, and the 3rd is what makes it
	// sound like rock rather than metal.
	public static readonly int[][] MetalVoicings = { Power, PowerFlat7 };
	public static readonly int[] MetalVoicingWeights = { 4, 1 };

	// Degrees read against the (minor) scale: 5 = ♭VI, 6 = ♭VII, 1 = ♭II, 3 = iv. Metal takes the
	// ♭VI and phrygian ♭II moves — the darkest of the minor turnarounds, and the ones rock does
	// not reach for.
	public static readonly int[][] MetalProgressions =
	{
		new[] { 0, 6, 5, 6 }, // i–♭VII–♭VI–♭VII (driving)
		new[] { 0, 1, 0, 6 }, // i–♭II–i–♭VII (phrygian menace)
		new[] { 0, 0, 5, 6 }, // i pedal → ♭VI–♭VII
		new[] { 0, 5, 1, 0 }, // i–♭VI–♭II–i
		new[] { 0, 0, 1, 1 }, // i / ♭II pedal riff
		new[] { 0, 3, 5, 6 }, // i–iv–♭VI–♭VII
	};

	// Metal: a low pedal point under the riff, or the riff's own rhythm doubled. These are the
	// fallback tables — when the song draws the "follows the riff" mode the bass reads the
	// guitar's onsets instead of any of these (see Bass.cs), because both real metal bass modes
	// are RELATIONAL and a table can only ever approximate them.
	public static readonly Pattern[] MetalBass =
	{
		P( 0, 0, 0, 0, 0, 0, 0, 0,
		   0, 0, 0, 0, 0, 0, 0, Approach ),                               // pedal chug (2 bars)
		P( 0, Rest, Rest, Rest, Rest, Rest, Rest, Rest,
		   0, Rest, Rest, Rest, Rest, Rest, Rest, Approach ),             // whole-bar pedal point (2 bars)
		P( 0, 0, 12, 0, 0, 0, 12, 0,
		   0, 0, 12, 0, 0, 12, 0, Approach ),                             // octave gallop (2 bars)
	};

	// ── Punk harmony (Genre 4) ──
	// "Lean punk" / power-pop: overwhelmingly major, with the minor-key hardcore option as the
	// rare draw. Mixolydian went to ska — punk's grit comes from the tempo and the downstrokes,
	// not from a ♭7.
	public static readonly int[][] PunkScales =
	{
		new[] { 0, 2, 4, 5, 7, 9, 11 }, // major (the pop-punk default)
		new[] { 0, 2, 3, 5, 7, 8, 10 }, // natural minor (the darker hardcore draw)
	};
	public static readonly int[] PunkScaleWeights = { 5, 1 };

	// Punk is a power chord and, when it wants the anthem to open up, a plain triad. Nothing
	// added, nothing suspended — the voicing is the least interesting thing about a punk song.
	public static readonly int[][] PunkVoicings = { Power, Triad };
	public static readonly int[] PunkVoicingWeights = { 3, 2 };

	// Major degrees: 3 = IV, 4 = V, 5 = vi — the anthem turnarounds. Punk keeps the ones that
	// start on the tonic and drive; pop keeps the ones that start away from it and loop.
	public static readonly int[][] PunkProgressions =
	{
		new[] { 0, 4, 5, 3 }, // I–V–vi–IV (the pop-punk anthem)
		new[] { 0, 3, 4, 4 }, // I–IV–V–V
		new[] { 0, 4, 3, 4 }, // I–V–IV–V (three-chord drive)
		new[] { 0, 5, 4, 3 }, // I–vi–V–IV
		new[] { 3, 4, 0, 0 }, // IV–V–I–I (the run-up)
	};

	// Punk: relentless straight eighths — the one genre where the undifferentiated chug IS the
	// idiom. The variation is in the last bar of the phrase, not in the bar-to-bar.
	public static readonly Pattern[] PunkBass =
	{
		P( 0, 0, 0, 0, 0, 0, 0, 0,
		   0, 0, 0, 0, 0, 0, 0, 0,
		   0, 0, 0, 0, 0, 0, 0, 0,
		   0, 0, 0, 0, 0, 0, 0, Approach ),                               // eighth chug, 4-bar phrase
		P( 0, 0, 0, 0, 0, 0, 0, 0,
		   0, 0, 0, 0, 12, 12, 7, Approach ),                             // chug that pops the octave (2 bars)
		P( 0, 0, 7, 0, 0, 0, 7, Approach ),                               // root–fifth gallop (1 bar)
	};

	// ── Pop harmony (Genre 5) ──
	// Modern synth/dance-pop: bright major, with lydian's ♯4 as the shimmer. Lydian is pop's
	// alone — it is the one mode that reads as "produced" rather than played.
	public static readonly int[][] PopScales =
	{
		new[] { 0, 2, 4, 5, 7, 9, 11 }, // major
		new[] { 0, 2, 4, 6, 7, 9, 11 }, // lydian (the sparkly ♯4 — synth-pop shimmer)
	};
	public static readonly int[] PopScaleWeights = { 3, 1 };

	// Pop's chords are open and unresolved: add9 and sus2 leave the 3rd ambiguous, which is what
	// makes a four-chord loop sound like it never lands. The plain triad is the fallback.
	public static readonly int[][] PopVoicings = { Add9, Sus2, Triad };
	public static readonly int[] PopVoicingWeights = { 3, 2, 3 };

	// Pop owns the loops that do not begin on the tonic — the "Axis" rotations, which is exactly
	// what makes a four-chord pop loop sound endless rather than resolved.
	public static readonly int[][] PopProgressions =
	{
		new[] { 5, 3, 0, 4 }, // vi–IV–I–V (the "Axis" loop)
		new[] { 0, 5, 3, 4 }, // I–vi–IV–V
		new[] { 3, 0, 4, 5 }, // IV–I–V–vi
		new[] { 0, 3, 5, 4 }, // I–IV–vi–V
		new[] { 5, 3, 4, 0 }, // vi–IV–V–I
	};

	// Pop: a synth bass locked to the four-on-the-floor kick, with the octave pops that give a
	// dance track its bounce. One chord per bar here (ChordBars = 1), so these stay short and the
	// approach fires into every change.
	public static readonly Pattern[] PopBass =
	{
		P( 0, Rest, 0, Rest, 0, Rest, 0, Approach ),                      // root on every beat (1 bar)
		P( 0, Rest, 12, 0, Rest, 12, 0, Rest,
		   0, Rest, 12, 0, Rest, 12, 7, Approach ),                       // octave pops (2 bars)
		P( 0, Rest, Rest, 0, Rest, 0, Rest, Approach ),                   // sidechained syncopation (1 bar)
	};

	/// <summary>Degree → MIDI pitch against <paramref name="scale"/>, wrapping octaves in both
	/// directions so any degree resolves.</summary>
	/// <summary>
	/// How far a bend travels, in semitones, from a note sitting <paramref name="pc"/> semitones
	/// above the tonic: to the nearest tone OF THE SCALE at or beyond <paramref name="depth"/>
	/// semitones up, and never back to the note it started from. 0 means nothing in reach.
	///
	/// A PLAYER BENDS TO A NOTE, NOT BY AN INTERVAL. The string arrives at the next tone of the
	/// scale, which is a whole step in some places and a semitone in others — the same fact about
	/// seven-note scales that <see cref="VoicedTone"/> exists for, reached through the melody
	/// instead of through a chord. Bent by a fixed interval the note lands off the key on every
	/// degree whose step is the other size: a whole step off the third or the seventh of a major
	/// scale, a semitone off almost anywhere. It is worst on a bend that is HELD, because the note
	/// then spends its whole tail outside the key rather than passing through it — which is what
	/// "out of tune" sounds like when nothing has actually been mistuned.
	///
	/// Depth stays the instrument's PREFERENCE — how far the hand reaches, which is the thing a
	/// genre has an opinion about — rather than the distance the pitch travels.
	/// </summary>
	public static int BendSemis( int[] scale, int pc, float depth )
	{
		int want = Math.Max( 1, (int)MathF.Round( depth ) );
		int best = 0, bestCost = int.MaxValue;
		for ( int s = 1; s <= BendReach; s++ )
		{
			bool inKey = false;
			foreach ( int t in scale )
				if ( (((t % 12) + 12) % 12) == (pc + s) % 12 ) { inKey = true; break; }
			if ( !inKey ) continue;
			int cost = Math.Abs( s - want );
			if ( cost >= bestCost ) continue;
			bestCost = cost; best = s;
		}
		return best;
	}

	/// <summary>How far up a bend will look for a tone of the scale. A seven-note scale has one
	/// within two semitones of anywhere, so this is slack for the pentatonic and blues tables
	/// rather than a range a bend actually reaches — past it there is no note to arrive at and
	/// the bend does not happen.</summary>
	public const int BendReach = 4;

	public static int ScaleMidi( int baseMidi, int[] scale, int degree )
	{
		int len = scale.Length;
		int oct = (int)Math.Floor( degree / (double)len );
		return baseMidi + scale[degree - oct * len] + 12 * oct;
	}

	/// <summary>Root pitch of a progression degree.</summary>
	public static int ChordRoot( int rootMidi, int[] scale, int degree )
		=> ScaleMidi( rootMidi, scale, degree );

	/// <summary>
	/// One voice of a chord, as a MIDI pitch — the ONLY correct way to turn a voicing into notes.
	///
	/// A voicing is a list of degree offsets, so <c>ScaleMidi(base, root + offset)</c> spells it
	/// DIATONICALLY: every interval comes out whatever the scale makes it at that degree. For the
	/// third, the sixth, the seventh and the ninth that is exactly right — major-or-minor by
	/// position is what diatonic harmony IS. For the FOURTH and the FIFTH it is wrong, because
	/// every seven-note scale has one degree whose diatonic fifth is DIMINISHED and one whose
	/// fourth is AUGMENTED. Spelled diatonically, a power chord on that degree is a bare tritone
	/// and a sus4 is a root with a flat five — with no third present to explain either, which is
	/// what "way off key" sounds like, and it is at its worst through distortion.
	///
	/// A guitarist frets the same power-chord shape on every degree; the shape does not go
	/// diminished because of the key. So the fourth and the fifth are forced perfect and
	/// everything else keeps its diatonic spelling. Both offsets sit inside one octave of the
	/// root, so the perfect interval is simply the root plus 5 or 7.
	/// </summary>
	public static int VoicedTone( int baseMidi, int[] scale, int rootDegree, int offset )
	{
		int root = ScaleMidi( baseMidi, scale, rootDegree );
		if ( offset == Fourth ) return root + 5;
		if ( offset == Fifth ) return root + 7;
		return ScaleMidi( baseMidi, scale, rootDegree + offset );
	}

	/// <summary>How far one voice may be octave-shifted to stay near the chord before it. An
	/// octave is what an INVERSION is; more than that moves the part into another register
	/// rather than re-voicing the chord.</summary>
	public const int MaxVoiceLead = 12;

	/// <summary>
	/// The song's chord plan: for every chord of the progression, WHICH note of the voicing each
	/// voice takes and how far it is octave-shifted.
	///
	/// The two are one decision. Octave-shifting alone kills the octave-sized parallel leap but
	/// leaves voice <c>i</c> permanently on the <c>i</c>th offset of the voicing, so a root move of
	/// a fourth still moves every voice by that fourth — a smaller parallel slide rather than none,
	/// and no common tone anywhere in it. Letting the chord ROTATE is what produces a common tone:
	/// voice <c>i</c> plays offset <c>(i + Rot[c]) mod n</c>, so the voice that was on the fifth can
	/// take the new chord's root and simply stay where it is.
	/// </summary>
	public readonly struct VoicePlan
	{
		/// <summary>Per chord, per voice: the octave offset in semitones.</summary>
		public readonly int[][] Shift;

		/// <summary>Per chord: voice <c>i</c> plays voicing offset <c>(i + Rot[c]) mod n</c>.</summary>
		public readonly int[] Rot;

		public VoicePlan( int[][] shift, int[] rot ) { Shift = shift; Rot = rot; }
	}

	/// <summary>
	/// VOICE LEADING: per chord of <paramref name="prog"/>, the octave offset each voice of
	/// <paramref name="voicing"/> takes so the chord sits near the one before it.
	///
	/// Built upward from its root degree, a chord's register is wherever that degree happens to
	/// fall and the same shape simply slides — so a progression that steps a third moves every
	/// voice a tenth, with no common tone, every time the change comes round. That is what reads
	/// as "it jumped" (and it is loudest when a chord change lands on a section boundary). A
	/// player inverts instead: keep the register, keep the common tones, move the voices that
	/// have to move.
	///
	/// Each voice is octave-shifted to whichever octave sits nearest its OWN previous pitch (ties
	/// going to the octave nearer root position, so the comp cannot walk itself out of register
	/// over a few laps of the progression), so
	/// the chord's identity is untouched — same degrees, same spelling, different inversion. The
	/// result is a table because it is a property of the SONG (progression × voicing × scale),
	/// not of a voice: every chordal voice reads the same shifts and therefore agrees on the
	/// inversion, whatever register it plays in. The bass is deliberately NOT in here — it plays
	/// roots, and a root that inverts is a different chord.
	///
	/// A PROGRESSION IS A CYCLE, and the choice is made as one: the last chord going back round to
	/// the first is a change like any other, so a greedy walk anchored at the first chord parks
	/// every leap the other three avoided on that one seam. Relaxing the walk round and round does
	/// not fix it either — the cycle is what makes it oscillate rather than settle, and an
	/// unsettled seam is a leap of a seventh sitting in the middle of a fixed table. So each voice
	/// is solved EXACTLY, by a walk over the three octaves it may take at each chord that closes
	/// the loop (voices are independent, three options each, four chords: it is a handful of
	/// additions, done once per song). Ties go to the octave nearer root position, so the comp
	/// cannot walk itself out of register.
	///
	/// Pitches are measured over a base of 0 — the shift is base-independent — and a voice may dip
	/// a little under that base but no further (<see cref="VoiceLeadFloor"/>).
	/// </summary>
	public static VoicePlan PlanVoiceLeading( int[] scale, int[] prog, int[] voicing )
	{
		int n = voicing.Length, np = prog.Length;
		var shift = new int[np][];
		for ( int c = 0; c < np; c++ ) shift[c] = new int[n];

		// Root-position pitch of every voicing slot at every chord — the raw material both passes
		// below read.
		var tone = new int[np, n];
		for ( int c = 0; c < np; c++ )
			for ( int s = 0; s < n; s++ )
				tone[c, s] = VoicedTone( 0, scale, prog[c], voicing[s] );

		var rot = PlanRotation( tone, np, n );

		int k = 2 * (MaxVoiceLead / 12) + 1;              // the octaves on offer: −1, 0, +1
		var raw = new int[np];
		var cost = new int[np, k];
		var from = new int[np, k];
		var chain = new int[np];

		for ( int v = 0; v < n; v++ )
		{
			// Given the rotation, the voices are independent again: voice v simply plays whichever
			// slot the rotation hands it at each chord, and chooses its own octaves over that line.
			for ( int c = 0; c < np; c++ ) raw[c] = tone[c, (v + rot[c]) % n];

			int bestTotal = int.MaxValue;
			for ( int start = 0; start < k; start++ )     // the loop has to close on what it opened
			{
				if ( Pitch( raw[0], start ) < VoiceLeadFloor ) continue;
				for ( int c = 0; c < np; c++ )
					for ( int o = 0; o < k; o++ ) { cost[c, o] = Unreachable; from[c, o] = 0; }
				cost[0, start] = Home( start );

				for ( int c = 1; c < np; c++ )
					for ( int o = 0; o < k; o++ )
					{
						if ( Pitch( raw[c], o ) < VoiceLeadFloor ) continue;
						for ( int p = 0; p < k; p++ )
						{
							if ( cost[c - 1, p] >= Unreachable ) continue;
							int t = cost[c - 1, p] + Move( raw[c - 1], p, raw[c], o ) + Home( o );
							if ( t >= cost[c, o] ) continue;
							cost[c, o] = t;
							from[c, o] = p;
						}
					}

				for ( int last = 0; last < k; last++ )
				{
					if ( cost[np - 1, last] >= Unreachable ) continue;
					int total = cost[np - 1, last]
						+ (np > 1 ? Move( raw[np - 1], last, raw[0], start ) : 0);
					if ( total >= bestTotal ) continue;
					bestTotal = total;
					for ( int c = np - 1, o = last; c >= 0; c-- ) { chain[c] = o; o = from[c, o]; }
				}
			}
			for ( int c = 0; c < np; c++ ) shift[c][v] = 12 * (chain[c] - MaxVoiceLead / 12);
		}
		return new VoicePlan( shift, rot );

		int Pitch( int r, int o ) => r + 12 * (o - MaxVoiceLead / 12);
		// Motion is weighted so it always outranks the pull toward root position: an octave of
		// register is worth having, but never at the price of a semitone of extra movement.
		int Move( int ra, int a, int rb, int b ) => 2 * Math.Abs( Pitch( rb, b ) - Pitch( ra, a ) );
		int Home( int o ) => Math.Abs( o - MaxVoiceLead / 12 );
	}

	/// <summary>
	/// Which note of the voicing each voice takes, per chord — the rotation half of the plan.
	///
	/// Solved BEFORE the octaves and separately from them, which is what keeps the whole thing
	/// cheap. Solving both at once would make the state a rotation plus an octave for every voice
	/// (3^n × n rotations per chord), because a voice's octave at one chord is paid for on the edges
	/// either side of it. Split, each pass is small: this one walks n rotations over np chords, and
	/// the octave pass is then per-voice independent again.
	///
	/// The split is honest because of what this pass measures. It does not know the octaves yet, so
	/// it costs a voice's move as the distance it would travel IF it may invert freely — the
	/// interval folded into a tritone either way. That is exactly the question a rotation answers
	/// ("can this voice hold a common tone, or must it move?") and exactly the question the octave
	/// pass then answers concretely. A rotation that leaves a voice on the same pitch class costs
	/// zero here, which is a common tone, which is the point of the row.
	///
	/// A PROGRESSION IS A CYCLE, on the same argument as the octave pass: solved with the wrap from
	/// the last chord back to the first included, or every rotation the other changes avoided parks
	/// itself on that seam. Ties pull toward rotation 0, so a plan that gains nothing from rotating
	/// simply doesn't, and the voicing's own order — root at the bottom, as its table is written —
	/// survives wherever it is free to.
	/// </summary>
	static int[] PlanRotation( int[,] tone, int np, int n )
	{
		var rot = new int[np];
		if ( n < 2 || np < 2 ) return rot;

		var cost = new int[np, n];
		var from = new int[np, n];
		int bestTotal = int.MaxValue;

		for ( int start = 0; start < n; start++ )
		{
			for ( int c = 0; c < np; c++ )
				for ( int r = 0; r < n; r++ ) { cost[c, r] = Unreachable; from[c, r] = 0; }
			cost[0, start] = Home( start );

			for ( int c = 1; c < np; c++ )
				for ( int r = 0; r < n; r++ )
					for ( int p = 0; p < n; p++ )
					{
						if ( cost[c - 1, p] >= Unreachable ) continue;
						int t = cost[c - 1, p] + Move( c - 1, p, c, r ) + Home( r );
						if ( t >= cost[c, r] ) continue;
						cost[c, r] = t;
						from[c, r] = p;
					}

			for ( int last = 0; last < n; last++ )
			{
				if ( cost[np - 1, last] >= Unreachable ) continue;
				int total = cost[np - 1, last] + Move( np - 1, last, 0, start );
				if ( total >= bestTotal ) continue;
				bestTotal = total;
				for ( int c = np - 1, r = last; c >= 0; c-- ) { rot[c] = r; r = from[c, r]; }
			}
		}
		return rot;

		// Motion outranks the pull toward the unrotated order, the same way it does for octaves.
		int Move( int a, int ra, int b, int rb )
		{
			int sum = 0;
			for ( int v = 0; v < n; v++ )
				sum += Fold( tone[b, (v + rb) % n] - tone[a, (v + ra) % n] );
			return 2 * sum;
		}
		int Home( int r ) => r == 0 ? 0 : 1;
		// The interval a freely-inverting voice would actually travel: a minor seventh up is a whole
		// tone down, and the octave pass is what will choose which.
		static int Fold( int d )
		{
			d = ((d % 12) + 12) % 12;
			return Math.Min( d, 12 - d );
		}
	}

	/// <summary>
	/// Octave offsets that put a chord built on <paramref name="rootDegree"/> as near as possible to
	/// the pitches it follows — the one-off version of the plan above, for a chord that has no slot
	/// in the progression's cycle.
	///
	/// The ENDING is what needs it. A song's last chord used to be built in root position on the
	/// argument that a song should land where its genre voices the chord rather than where the last
	/// change happened to leave the register — which is a reasonable thing to want and was still
	/// wrong, because it put the only unled change in the song on its most exposed moment, and a
	/// seventh-sized leap into the final chord is heard by everyone. The register a song has been
	/// in for three minutes IS where it should land; a cadence is a change like any other, and the
	/// ritard is not a licence to jump.
	/// </summary>
	public static int[] LeadToward( int[] scale, int[] prev, int baseMidi, int rootDegree, int[] voicing )
	{
		var shift = new int[voicing.Length];
		if ( prev == null || prev.Length == 0 ) return shift;
		for ( int i = 0; i < voicing.Length; i++ )
		{
			int raw = VoicedTone( baseMidi, scale, rootDegree, voicing[i] );
			// Nearest to the voice that was on the same line, where there was one; the chord may be
			// spelled with more notes than the one before it (a suspension resolving, a driven
			// guitar), so anything past the end leans on the top voice.
			int target = prev[Math.Min( i, prev.Length - 1 )];
			int best = 0, bestCost = int.MaxValue;
			for ( int o = -MaxVoiceLead; o <= MaxVoiceLead; o += 12 )
			{
				if ( raw + o < baseMidi + VoiceLeadFloor ) continue;
				int cost = 2 * Math.Abs( raw + o - target ) + Math.Abs( o ) / 12;
				if ( cost >= bestCost ) continue;
				bestCost = cost; best = o;
			}
			shift[i] = best;
		}
		return shift;
	}

	/// <summary>How far under its own base a voice may be led, relative to the base the chord is
	/// voiced up from. A chord whose root is the scale's seventh degree sits eleven semitones up in
	/// root position and one semitone DOWN inverted, and the inverted one is the whole point — a
	/// floor at the base exactly forbids the move that helps most. Half an octave under is where
	/// "inverted" turns into "an octave lower", and metal's comp is based at the bass's own
	/// register, so there is no room below that.</summary>
	public const int VoiceLeadFloor = -6;

	/// <summary>Cost of a path that does not exist — larger than any real one, and small enough to
	/// add to another without overflowing.</summary>
	const int Unreachable = 1 << 20;
}

public sealed partial class MusicGen
{
	// The song's own scale/progression, supplied to the stateless Harmony maths. Every voice
	// calls these rather than reaching for _scale directly. The section's KeyShift rides on the
	// root here, so a modulation moves the whole band at once (see Part.KeyShift).
	int ScaleMidi( int baseMidi, int degree ) => Harmony.ScaleMidi( baseMidi, _scale, degree );

	/// <summary>A voice's register: the pitch it spells its scale over, <paramref name="octaves"/>
	/// octaves above the song's root.
	///
	/// A REGISTER IS A NUMBER OF OCTAVES, AND ONLY EVER THAT. <see cref="Harmony.ScaleMidi"/> and
	/// <see cref="Harmony.VoicedTone"/> treat their base as THE TONIC and add the scale offset on
	/// top, so a base of root + 31 does not raise a part by a fifth — it spells that part in the
	/// key a fifth up. The part then disagrees with the band about one note of the scale (about two
	/// of them, a whole tone up), and a melody in a different key from its backing is exactly what
	/// it sounds like. Every voice takes its register through here so a base that is not a whole
	/// octave cannot be written in the first place, which is worth more than a test for it: the
	/// wrong version stays in tune roughly six notes in seven, so it does not announce itself.
	///
	/// The cost is that register is QUANTISED — a part sits an octave up or it doesn't, and there
	/// is no landing between. If a part ends up too high, narrow what it plays (a melody's degree
	/// range) rather than reaching for a base between two octaves.
	///
	/// Transposing an actual PITCH by an octave (<c>ChordRoot(c) + 12</c>) is a different thing and
	/// is fine — the scale has already been spelled by then.</summary>
	int Register( int octaves ) => _rootMidi + _keyShift + 12 * octaves;
	int ChordRoot( int c ) => Harmony.ChordRoot( _rootMidi + _keyShift, _scale, _prog[c] );

	/// <summary>Degree of the <paramref name="i"/>th voice of the chord, in the song's own
	/// voicing. Indices past the top wrap up an octave, so an arpeggio can just keep counting.
	/// </summary>
	int ChordDegree( int chord, int i )
	{
		int n = _voicing.Length;
		int oct = (int)Math.Floor( i / (double)n );
		return _prog[chord] + _voicing[i - oct * n] + oct * _scale.Length;
	}

	/// <summary>Every note of the chord, as scale degrees. This is the MELODIC view — what tones
	/// a line may land on. A voice that SOUNDS the chord wants <see cref="ChordMidis"/> instead,
	/// which keeps the perfect intervals perfect.</summary>
	int[] ChordDegrees( int chord )
	{
		var d = new int[_voicing.Length];
		for ( int i = 0; i < d.Length; i++ ) d[i] = _prog[chord] + _voicing[i];
		return d;
	}

	/// <summary>Every note of the chord as MIDI pitches over <paramref name="baseMidi"/> — what a
	/// chordal voice actually plays. Use this rather than <c>ScaleMidi</c> over
	/// <see cref="ChordDegrees"/>: see <see cref="Harmony.VoicedTone"/> for why the difference
	/// matters on one degree of every scale.
	///
	/// This is also where the song's VOICE LEADING lands (<see cref="Harmony.PlanVoiceLeading"/>),
	/// so the chord arrives in the inversion nearest the one before it instead of sliding a tenth.
	/// The array index is a VOICE, not a voicing slot — the plan rotates, so which offset voice i
	/// plays is <c>(i + Rot[chord]) mod n</c> and changes chord to chord. Anything that needs to
	/// know what a pitch IS asks <see cref="ChordOffsets"/> for the matching offsets rather than
	/// indexing <c>_voicing</c> alongside it; the driven guitar is the one that does.</summary>
	int[] ChordMidis( int baseMidi, int chord, int tick )
	{
		var voicing = VoicingAt( tick );
		int n = voicing.Length, r = _vlRot[chord];
		var s = _vlShift[chord];
		var m = new int[n];
		for ( int i = 0; i < n; i++ )
			m[i] = Harmony.VoicedTone( baseMidi, _scale, _prog[chord], voicing[(i + r) % n] ) + s[i];
		return m;
	}

	/// <summary>The voicing offsets <see cref="ChordMidis"/> just spelled, in the same order — what
	/// each of its pitches IS. A rotated chord has to carry these alongside its pitches, because
	/// the array index no longer names the voicing slot: without them the driven guitar drops
	/// whatever happens to be third in the array rather than the chord's third.</summary>
	int[] ChordOffsets( int chord, int tick )
	{
		var voicing = VoicingAt( tick );
		int n = voicing.Length, r = _vlRot[chord];
		var o = new int[n];
		for ( int i = 0; i < n; i++ ) o[i] = voicing[(i + r) % n];
		return o;
	}

	/// <summary>The spelling the chordal voices sound at <paramref name="tick"/>: the song's
	/// voicing, or — over the back half of the current chord's span — the one its suspension
	/// resolves to (<see cref="Harmony.SuspendedVoice"/>). The two arrays are the same object
	/// unless the voicing is suspended, so this costs nothing for the other five voicings.
	///
	/// EVERY chordal voice reads it, at the tick of the note it is about to sound, so the band
	/// resolves together the way it agrees on the chord and the inversion. The voice-leading table
	/// is deliberately NOT recomputed: a suspension resolving moves one voice a step inside the
	/// inversion the song already chose, which is what a player's finger does — re-inverting the
	/// chord underneath it would make the landing a jump.</summary>
	int[] VoicingAt( int tick ) => tick >= _susResolveTick ? _voicingRes : _voicing;

	/// <summary>A chordal note of <paramref name="durTicks"/> from <paramref name="tick"/>, split
	/// into the part before its suspension resolves and the part after — one segment for every
	/// note that does not straddle the resolution, which is all of them for a voicing that is not
	/// suspended.
	///
	/// The chord is RE-ARTICULATED where the third lands rather than changing under a ringing
	/// note, because a suspension that resolves silently is not heard to resolve: the landing is
	/// the gesture, and a player re-picks or hammers the note that moves. It matters most where a
	/// voice holds a whole chord at a time — pop's pad sounds one chord per bar, so without this
	/// its suspension has nowhere to land at all.
	///
	/// The chordal voices whose genres can draw a suspension (the guitar and the keys) read this;
	/// ska's skank and horns do not, because every ska voicing states its third.</summary>
	IEnumerable<(int Tick, int Ticks)> ChordSegments( int tick, int durTicks )
	{
		if ( _susVoice >= 0 && tick < _susResolveTick && tick + durTicks > _susResolveTick )
		{
			yield return (tick, _susResolveTick - tick);
			yield return (_susResolveTick, tick + durTicks - _susResolveTick);
			yield break;
		}
		yield return (tick, durTicks);
	}

	/// <summary>As <see cref="ChordMidis"/> but in ROOT POSITION, for a chord whose root degree is
	/// given directly — the ending's cadence builds its V that way, and a final chord lands where
	/// the genre voices it rather than where the last change left the register.</summary>
	int[] VoicedMidis( int baseMidi, int rootDegree, int[] voicing )
	{
		var m = new int[voicing.Length];
		for ( int i = 0; i < m.Length; i++ )
			m[i] = Harmony.VoicedTone( baseMidi, _scale, rootDegree, voicing[i] );
		return m;
	}

	/// <summary>Pitch of the <paramref name="i"/>th voice of the chord (wrapping up an octave past
	/// the top, so an arpeggio can keep counting) — the sounding counterpart of
	/// <see cref="ChordDegree"/>.</summary>
	int ChordToneMidi( int baseMidi, int chord, int i, int tick )
	{
		var voicing = VoicingAt( tick );
		int n = voicing.Length;
		int oct = (int)Math.Floor( i / (double)n );
		int v = i - oct * n;
		return Harmony.VoicedTone( baseMidi, _scale, _prog[chord], voicing[(v + _vlRot[chord]) % n] )
			+ _vlShift[chord][v] + 12 * oct;
	}
}
gamah.skafinity / Engine/Rng.cs
Game library
using System;

namespace Skafinity;

/// <summary>
/// The engine's PRNG — xmur3 hashes a seed string to 32 bits, mulberry32 streams from it.
/// Every musical choice comes out of here, which is why one seed gives one song.
///
/// Deliberately hand-rolled rather than <c>System.Random</c>: the algorithm is pinned by this
/// file, so it does not move under us when a runtime changes its implementation, and the same
/// source gives the same stream in the s&amp;box library and the wasm bundle alike.
///
/// Streams are cheap and are meant to be forked liberally — the composer gives each voice in
/// each section its own <c>Rng</c> keyed on the section, so adding a draw to one voice cannot
/// shift what any other voice plays.
/// </summary>
sealed class Rng
{
	uint _a;

	public Rng( uint seed ) { _a = seed; }

	/// <summary>A stream keyed on a string — the composer's usual entry point
	/// (<c>"{tag}:bass:{section}"</c> and friends).</summary>
	public Rng( string seed ) : this( Xmur3( seed ) ) { }

	/// <summary>xmur3: string → a well-mixed 32-bit seed.</summary>
	public static uint Xmur3( string str )
	{
		uint h = 1779033703u ^ (uint)str.Length;
		for ( int i = 0; i < str.Length; i++ )
		{
			h = unchecked( (h ^ str[i]) * 3432918353u );
			h = (h << 13) | (h >> 19);
		}
		h = unchecked( (h ^ (h >> 16)) * 2246822507u );
		h = unchecked( (h ^ (h >> 13)) * 3266489909u );
		return h ^ (h >> 16);
	}

	/// <summary>mulberry32: the next value in [0, 1).</summary>
	public float Next()
	{
		_a = unchecked( _a + 0x6D2B79F5u );
		uint t = _a;
		t = unchecked( (t ^ (t >> 15)) * (t | 1u) );
		t ^= unchecked( t + (t ^ (t >> 7)) * (t | 61u) );
		return (t ^ (t >> 14)) / 4294967296f;
	}

	/// <summary>A value in [0, n). Clamped, so it is always a safe table index.</summary>
	public int Int( int n ) => n <= 0 ? 0 : Math.Min( n - 1, (int)(Next() * n) );

	public bool Chance( float p ) => Next() < p;

	public T Pick<T>( T[] arr ) => arr[Int( arr.Length )];

	/// <summary>A weighted draw — ONE <see cref="Next"/> whatever the weights are.
	///
	/// The tables used to bias a draw by listing an entry twice, which quietly tied "how likely"
	/// to "how many entries" (and to the no-two-genres-share-more-than-one cap the engine test
	/// enforces). A real weighted draw separates them, and costs the same single value out of
	/// the song stream, so a genre's draw count still cannot depend on its tables.</summary>
	/// <summary>The INDEX of a weighted draw — one <see cref="Next"/>, like
	/// <see cref="PickWeighted"/>, for callers whose table is a parallel array rather than the
	/// thing being picked (the melody's note lengths against a genre's weights over them).</summary>
	public int WeightedIndex( int[] weights )
	{
		if ( weights == null || weights.Length == 0 ) return 0;
		int total = 0;
		foreach ( var w in weights ) total += Math.Max( 0, w );
		if ( total <= 0 ) return Int( weights.Length );
		float r = Next() * total;
		for ( int i = 0; i < weights.Length; i++ )
		{
			r -= Math.Max( 0, weights[i] );
			if ( r < 0f ) return i;
		}
		return weights.Length - 1;
	}

	public T PickWeighted<T>( T[] arr, int[] weights )
	{
		if ( arr == null || arr.Length == 0 ) return default;
		if ( weights == null || weights.Length != arr.Length ) return Pick( arr );
		int total = 0;
		foreach ( var w in weights ) total += Math.Max( 0, w );
		if ( total <= 0 ) return Pick( arr );
		float r = Next() * total;
		for ( int i = 0; i < arr.Length; i++ )
		{
			r -= Math.Max( 0, weights[i] );
			if ( r < 0f ) return arr[i];
		}
		return arr[arr.Length - 1];
	}
}
gamah.skafinity / Engine/SeedCodec.cs
Game library
using System;
using System.Text;

namespace Skafinity;

/// <summary>
/// The seed STRING — what a whole song is, and the only thing that has to travel between two
/// people for them to hear the same thing.
///
///   <c>tag:n[:genre][:vibe]</c>
///
/// * <c>tag</c> — the station: <c>[A-Za-z0-9_-]</c> only. Anything else (a colon, a space, a
///   slash) is a parse ERROR, never a coerced string, because a seed that quietly becomes a
///   different seed is worse than one that is refused.
/// * <c>n</c> — the song index in that station's endless line, and it keeps the job it has always
///   had: Prev/Next are n±1, the look-ahead queue walks an ordered timeline of them, and n is what
///   lets you go back to a song fifty ago that nothing anywhere remembers. Optional in the string
///   (a bare <c>tag</c> is song 0 of that station), because typing a station name is how you go
///   somewhere new.
/// * <c>genre</c> and <c>vibe</c> — both optional, both hex, and ORDER-FREE: they are told apart
///   by length. One char is a genre; <see cref="VibeCodec.VibeLength"/> chars is a vibe. Any other
///   length is an error rather than a guess.
///
/// ABSENT MEANS ROLLED. An omitted genre or vibe is derived deterministically from (tag, n), so it
/// changes with every song and the station stays a station. Present means PINNED. The two roll
/// from separate streams, so pinning one does not move the other: pin a vibe and let genres roll,
/// pin a genre and let vibes roll, pin both and move only n.
///
/// That is why the vibe is genre-independent and full width (see <see cref="VibeCodec"/>) — a
/// pinned vibe has to mean the same thing under a genre that rolled out from under it.
/// </summary>
public static class SeedCodec
{
	/// <summary>No genre pinned — roll it from (tag, n).</summary>
	public const int RolledGenre = -1;

	/// <summary>A parsed seed. <see cref="Genre"/> is <see cref="RolledGenre"/> and
	/// <see cref="Vibe"/> is null where the string pinned nothing.</summary>
	public struct Seed
	{
		public string Tag;
		public int N;
		public int Genre;
		public string Vibe;

		public bool GenrePinned => Genre >= 0;
		public bool VibePinned => Vibe != null;
	}

	/// <summary>The station a tag names: trimmed and lower-cased, so "Gamah" and " gamah " are one
	/// station rather than three, with the default for an empty tag.</summary>
	/// <remarks>Every stream name in the toy is built from this, on BOTH targets, because the
	/// fallback word is load-bearing: it is part of what song a seed with no tag resolves to, and
	/// a host that picks its own makes <c>:23</c> a different song there than everywhere else. It
	/// has been exactly that — the s&amp;box player spelled the fallback "skafinity" while the
	/// engine and the web spelled it "rotaliate".</remarks>
	public static string Station( string tag ) =>
		string.IsNullOrWhiteSpace( tag ) ? "rotaliate" : tag.Trim().ToLowerInvariant();

	/// <summary>The PRNG stream song <paramref name="n"/> is COMPOSED from — what a host hands
	/// <see cref="MusicGen.Generate"/>/<see cref="MusicGen.BeginPlan"/> as the tag.</summary>
	public static string SongSeed( string tag, int n ) => $"{Station( tag )}:{n}";

	/// <summary>The stream song <paramref name="n"/>'s VIBE is rolled from.</summary>
	public static string VibeSeed( string tag, int n ) => $"{Station( tag )}:vibe:{n}";

	/// <summary>The stream song <paramref name="n"/>'s GENRE is rolled from. Separate from
	/// <see cref="VibeSeed"/> on purpose: pinning the vibe must not change which genres a station
	/// plays, and pinning the genre must not change which vibes it rolls.</summary>
	public static string GenreSeed( string tag, int n ) => $"{Station( tag )}:genre:{n}";

	/// <summary>Song <paramref name="n"/>'s rolled vibe — the same string on any machine, in any
	/// player, forever. This is what lets an endless line still BE its seed: nothing has to be
	/// remembered for Prev to replay exactly what was heard.</summary>
	public static string RollVibeFor( string tag, int n )
	{
		var rng = new Rng( VibeSeed( tag, n ) );
		return VibeCodec.RollVibe( rng.Next );
	}

	/// <summary>Song <paramref name="n"/>'s rolled genre.</summary>
	public static int RollGenreFor( string tag, int n )
	{
		var rng = new Rng( GenreSeed( tag, n ) );
		return VibeCodec.RollGenre( rng.Next );
	}

	/// <summary>
	/// The station at position <paramref name="p"/> of a SHUFFLED line — a player that answers each
	/// "next" with a whole new station rather than the next song of this one.
	///
	/// It is DERIVED from the root rather than drawn fresh, and that is the whole design: a random
	/// tag per song would make the line unrepeatable, so Prev could only work by remembering every
	/// station visited, and a reload would lose the lot. Derived, the shuffled line is still just a
	/// seed — walkable in both directions, the same everywhere, and reproducible from one string.
	///
	/// Position 0 is the root itself, so a pasted seed plays the song it names before the shuffle
	/// takes over.
	/// </summary>
	public static string RollTagFor( string root, int p )
	{
		if ( p <= 0 ) return root ?? "";
		var rng = new Rng( $"{Station( root )}:tag:{p}" );
		var sb = new StringBuilder( 8 );
		for ( int i = 0; i < 8; i++ )
		{
			int q = Math.Clamp( (int)(rng.Next() * 36), 0, 35 );
			sb.Append( q < 10 ? (char)('0' + q) : (char)('a' + q - 10) );
		}
		return sb.ToString();
	}

	// ── Parsing ───────────────────────────────────────────────────────────────────────────

	static bool IsTagChar( char ch ) =>
		(ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9')
		|| ch == '_' || ch == '-';

	static bool IsHex( string s )
	{
		foreach ( var ch in s )
			if ( VibeCodec.Hex.IndexOf( char.ToLowerInvariant( ch ) ) < 0 ) return false;
		return true;
	}

	/// <summary>Parse a seed string. On failure <paramref name="error"/> is a sentence fit to show
	/// a listener under the seed box, and <paramref name="seed"/> is left at its default — there is
	/// no partial success, because half a seed is a song nobody asked for.</summary>
	public static bool TryParse( string s, out Seed seed, out string error )
	{
		seed = default;
		error = null;
		s = (s ?? "").Trim();
		if ( s.Length == 0 ) { error = "a seed looks like tag:n"; return false; }

		var parts = s.Split( ':' );
		if ( parts.Length > 4 ) { error = "too many parts — tag:n[:genre][:vibe]"; return false; }

		foreach ( var ch in parts[0] )
			if ( !IsTagChar( ch ) )
			{
				error = $"'{ch}' is not allowed in a station name (letters, digits, _ and - only)";
				return false;
			}

		int n = 0;
		if ( parts.Length >= 2 )
		{
			if ( parts[1].Length == 0 ) { error = "the song number is missing"; return false; }
			foreach ( var ch in parts[1] )
				if ( ch < '0' || ch > '9' ) { error = $"'{parts[1]}' is not a song number"; return false; }
			if ( !int.TryParse( parts[1], out n ) ) { error = "that song number is too big"; return false; }
		}

		int genre = RolledGenre;
		string vibe = null;
		for ( int i = 2; i < parts.Length; i++ )
		{
			var p = parts[i];
			if ( p.Length == 0 ) { error = "an empty part — drop the extra ':'"; return false; }
			if ( !IsHex( p ) ) { error = $"'{p}' is not hex (0-9, a-f)"; return false; }
			if ( p.Length == 1 )
			{
				if ( genre != RolledGenre ) { error = "two genres in one seed"; return false; }
				int g = VibeCodec.Hex.IndexOf( char.ToLowerInvariant( p[0] ) );
				if ( g >= VibeCodec.GenreCount )
				{
					error = $"there is no genre '{p}' (0-{VibeCodec.Hex[VibeCodec.GenreCount - 1]})";
					return false;
				}
				genre = g;
			}
			else if ( p.Length == VibeCodec.VibeLength )
			{
				if ( vibe != null ) { error = "two vibes in one seed"; return false; }
				vibe = p.ToLowerInvariant();
			}
			else
			{
				error = $"a vibe is {VibeCodec.VibeLength} characters, not {p.Length}";
				return false;
			}
		}

		seed = new Seed { Tag = parts[0], N = n, Genre = genre, Vibe = vibe };
		return true;
	}

	/// <summary>The canonical string for a seed: pinned parts written genre-then-vibe, rolled parts
	/// left out. Round-trips through <see cref="TryParse"/>.</summary>
	public static string Format( Seed seed )
	{
		var sb = new StringBuilder();
		sb.Append( seed.Tag ?? "" ).Append( ':' ).Append( Math.Max( 0, seed.N ) );
		if ( seed.GenrePinned ) sb.Append( ':' ).Append( VibeCodec.Hex[Math.Clamp( seed.Genre, 0, VibeCodec.GenreCount - 1 )] );
		if ( seed.VibePinned ) sb.Append( ':' ).Append( seed.Vibe );
		return sb.ToString();
	}

	/// <summary>The same seed with everything it left to chance written down — what "copy this
	/// song" hands over, as opposed to "copy this station", which is <see cref="Format"/> of the
	/// seed as it stands.</summary>
	public static Seed Resolved( Seed seed )
	{
		if ( !seed.GenrePinned ) seed.Genre = RollGenreFor( seed.Tag, seed.N );
		if ( !seed.VibePinned ) seed.Vibe = RollVibeFor( seed.Tag, seed.N );
		return seed;
	}

	/// <summary>Put a seed's song onto <paramref name="c"/>: its genre and its 36 knobs, pinned or
	/// rolled. Per-instrument volumes are NOT touched — they are a local mix preference, so a host
	/// overlays its own after this (<see cref="VibeCodec.ApplyVolumes"/>).</summary>
	public static void Apply( Seed seed, MusicGen.Config c )
	{
		if ( c == null ) return;
		var r = Resolved( seed );
		c.Genre = Math.Clamp( r.Genre, 0, VibeCodec.GenreCount - 1 );
		VibeCodec.Apply( r.Vibe, c );
	}
}
gamah.skafinity / Engine/Wav.cs
Game library
using System;
using System.Collections.Generic;

namespace Skafinity;

/// <summary>
/// The WAV container — 16-bit PCM, the one format both targets can hand straight to a player.
/// Stateless: it wraps samples somebody else rendered.
/// </summary>
static class Wav
{
	/// <summary>Clamp a −1..1 mix sample to signed 16-bit.</summary>
	public static short ToS16( float v ) => (short)(Math.Clamp( v, -1f, 1f ) * 32767f);

	/// <summary>Wrap already-rendered 16-bit samples in a WAV. Mono or interleaved stereo per
	/// <paramref name="channels"/>.</summary>
	public static byte[] FromSamples( short[] samples, int channels, int sampleRate )
	{
		int dataSize = samples.Length * 2;
		int blockAlign = channels * 2;
		var bytes = new List<byte>( 44 + dataSize );
		void Str( string s ) { foreach ( var ch in s ) bytes.Add( (byte)ch ); }
		void U32( uint v ) { bytes.Add( (byte)v ); bytes.Add( (byte)(v >> 8) ); bytes.Add( (byte)(v >> 16) ); bytes.Add( (byte)(v >> 24) ); }
		void U16( ushort v ) { bytes.Add( (byte)v ); bytes.Add( (byte)(v >> 8) ); }
		Str( "RIFF" ); U32( (uint)(36 + dataSize) ); Str( "WAVE" );
		Str( "fmt " ); U32( 16 ); U16( 1 ); U16( (ushort)channels );
		U32( (uint)sampleRate ); U32( (uint)(sampleRate * blockAlign) ); U16( (ushort)blockAlign ); U16( 16 );
		Str( "data" ); U32( (uint)dataSize );
		foreach ( var s in samples ) { ushort u = (ushort)s; bytes.Add( (byte)u ); bytes.Add( (byte)(u >> 8) ); }
		return bytes.ToArray();
	}
}

public sealed partial class MusicGen
{
	// ── Output ──
	short[] ToShorts( float gain )
	{
		int n = _bufL.Length;
		var s = new short[n * Channels];
		for ( int i = 0; i < n; i++ )
		{
			s[i * 2] = Wav.ToS16( _bufL[i] * gain );
			s[i * 2 + 1] = Wav.ToS16( _bufR[i] * gain );
		}
		return s;
	}

	/// <summary>Wrap already-rendered 16-bit samples in a WAV (for export). Mono or
	/// interleaved stereo per <paramref name="channels"/>.</summary>
	public static byte[] WavFromSamples( short[] samples, int channels, int sampleRate )
		=> Wav.FromSamples( samples, channels, sampleRate );

	byte[] EncodeWav( float gain ) => Wav.FromSamples( ToShorts( gain ), Channels, _sr );
}
gamah.skafinity / UI/SkafinityBoard.cs
Game library
using System;
using System.Collections.Generic;

namespace Skafinity;

/// <summary>
/// The board's PRESENTATION RULES, with no widget toolkit in sight: what the controls are called,
/// what they say when they change, how a length or a playlist row is worded, and how the vibe's
/// fields fall into a grid. Everything here is a pure function of engine state.
/// </summary>
/// <remarks>
/// <para>This exists because the same board is drawn twice — once as a Razor panel here, once as
/// <c>web/skafinity-element.js</c> — and the half that drifts between two drawings of one design is
/// never the layout, it is the wording and the small derived decisions: which button is disabled,
/// what a row says when nothing is cached, whether a knob repeats its column's name. Those are
/// written down once, here.</para>
///
/// <para><b>Framework-free on purpose, and not yet shared.</b> Nothing in this file touches
/// <c>Sandbox.*</c>, so it can move under <c>Code/Engine/</c> — the folder both targets compile —
/// the day the web asks the wasm for it instead of keeping its own copy. It is deliberately NOT
/// there yet: a file under <c>Engine/</c> is a file in the wasm bundle, and adding one costs a full
/// AOT re-stage (see the stale-bundle gate in CLAUDE.md) for code nothing on that side calls yet.
/// Keep it free of engine-target-hostile types so that move stays a move rather than a rewrite.</para>
/// </remarks>
public static class SkafinityBoard
{
	// ── The grid ────────────────────────────────────────────────────────────────────────────────
	/// <summary>One header per vibe-matrix column. Column 0 (VOLUME) is a local mix preference and
	/// never travels; columns 1..4 are the wire. The grid is rectangular, so a voice with nothing in
	/// its last column simply leaves that cell empty.</summary>
	public static readonly string[] ColumnHeaders = { "VOLUME", "TONE", "CHARACTER", "EXTRA", "MORE" };

	/// <summary>One row of the per-instrument mixer: a voice and its cells, one per
	/// <see cref="ColumnHeaders"/> entry, null where this genre leaves a column empty.</summary>
	public readonly struct MatrixRow
	{
		/// <summary>Voice name — the row label (BASS, DRUMS, …).</summary>
		public string Voice { get; init; }
		/// <summary>Cells by column index; null = this genre has no knob there.</summary>
		public VibeCodec.Field[] Cells { get; init; }
	}

	/// <summary>Lay the genre's vibe fields out as the mixer grid: one row per voice, in the
	/// library's own display order. Fields with no voice are GLOBAL and come back from
	/// <see cref="Globals"/> instead.</summary>
	/// <remarks>Driven entirely from the field metadata, so a new genre — or a new knob — is a pure
	/// engine change and there is no field table in any UI.</remarks>
	public static List<MatrixRow> Matrix( int genre )
	{
		var order = new List<string>();
		var byVoice = new Dictionary<string, VibeCodec.Field[]>();
		foreach ( var f in VibeCodec.Fields( genre ) )
		{
			if ( f.Voice == null ) continue;
			if ( !byVoice.TryGetValue( f.Voice, out var cells ) )
			{
				cells = new VibeCodec.Field[ColumnHeaders.Length];
				byVoice[f.Voice] = cells;
				order.Add( f.Voice );
			}
			if ( f.Column >= 0 && f.Column < cells.Length ) cells[f.Column] = f;
		}
		var rows = new List<MatrixRow>( order.Count );
		foreach ( var v in order ) rows.Add( new MatrixRow { Voice = v, Cells = byVoice[v] } );
		return rows;
	}

	/// <summary>The genre's knobs that belong to no instrument — the GLOBAL strip under the grid.
	/// Often empty (the globals have been retired to reserved wire slots), and a heading over an
	/// empty grid reads as a panel that failed to draw something, so callers check.</summary>
	public static List<VibeCodec.Field> Globals( int genre )
	{
		var list = new List<VibeCodec.Field>();
		foreach ( var f in VibeCodec.Fields( genre ) )
			if ( f.Voice == null ) list.Add( f );
		return list;
	}

	/// <summary>What to write above a knob in the grid: nothing when the column header already says
	/// it (VOLUME under VOLUME reads as a mistake), the field's own name otherwise.</summary>
	public static string KnobLabel( VibeCodec.Field f, int column ) =>
		f == null ? "" : f.Name == ColumnHeaders[column] ? "" : f.Name;

	/// <summary>Index of a field within its genre's field list — what
	/// <see cref="SkafinityPlayer.SetVibe"/> takes.</summary>
	public static int FieldIndex( int genre, VibeCodec.Field field )
	{
		var fields = VibeCodec.Fields( genre );
		for ( int i = 0; i < fields.Count; i++ )
			if ( ReferenceEquals( fields[i], field ) ) return i;
		return -1;
	}

	/// <summary>Which of a choice field's options a 0..1 value selects.</summary>
	public static int ChoiceIndex( VibeCodec.Field f, float norm ) =>
		f?.Choices == null ? 0
		: Math.Clamp( (int)MathF.Round( norm * (f.Choices.Length - 1) ), 0, f.Choices.Length - 1 );

	/// <summary>…and the 0..1 value that selects option <paramref name="k"/>.</summary>
	public static float ChoiceNorm( VibeCodec.Field f, int k ) =>
		f?.Choices == null || f.Choices.Length < 2 ? 0f : k / (float)(f.Choices.Length - 1);

	// ── Numbers as words ────────────────────────────────────────────────────────────────────────
	/// <summary>Shown where a length would be if there were one. A song that has not been rendered
	/// has no length to state, and an honest dash beats 0:00 — which reads as a song of no length.</summary>
	public const string NoTime = "–:––";

	/// <summary>m:ss, or <see cref="NoTime"/> when the length is not known yet.</summary>
	public static string Time( double seconds, bool known = true )
	{
		if ( !known || double.IsNaN( seconds ) ) return NoTime;
		int t = (int)Math.Round( Math.Max( 0, seconds ) );
		return $"{t / 60}:{(t % 60):00}";
	}

	/// <summary>A 0..1 fraction as a CSS width.</summary>
	public static string Percent( float f ) => $"{(int)MathF.Round( Math.Clamp( f, 0f, 1f ) * 100 )}%";

	/// <summary>Genre name for an id, or "?" — a UI drawing a row for a genre the engine does not
	/// have should show that rather than throw.</summary>
	public static string GenreName( int g ) =>
		g >= 0 && g < VibeCodec.GenreCount ? VibeCodec.Genres[g] : "?";

	/// <summary>The caret column of a playlist row: on the song you are hearing, nothing otherwise.
	/// A column rather than a prefix so every row's number starts at the same x.</summary>
	public static string RowCaret( SkafinityPlayer.QueueEntry e ) => e.Current ? "▶" : "";

	/// <summary>What a playlist row says on its right-hand side. Generating rows draw a bar instead
	/// and never reach this.</summary>
	public static string RowStatus( SkafinityPlayer.QueueEntry e ) =>
		e.Current ? Copy.RowNow : e.Cached ? Copy.RowReady : e.Past ? Copy.RowGone : Copy.RowPending;

	// ── The words ───────────────────────────────────────────────────────────────────────────────
	/// <summary>Every user-visible string on the board, in one place. The tooltips carry the reason a
	/// control exists, which is the part that is genuinely hard to reconstruct — "reroll" and
	/// "randomize" are both dice, and only their tooltips say why there are two.</summary>
	public static class Copy
	{
		// Transport
		public const string Prev = "⏮";
		public const string PrevTitle = "Previous song";
		public const string Play = "▶";
		public const string Pause = "⏸";
		public const string PlayTitle = "Play / Pause";
		public const string Next = "⏭";
		public const string NextTitle = "Next song";
		/// <summary>Ends in the hash on purpose — see <see cref="Hash"/>.</summary>
		public const string NowPlaying = "now playing #";
		/// <summary>A number sign, ALONE, as its own label.</summary>
		/// <remarks>A label whose text is LONGER than one character and begins with <c>#</c> is a
		/// localisation token: the engine looks the rest up as a phrase and renders what comes back,
		/// so <c>#24</c> silently becomes <c>24</c>. That is why no label here is built as "#" plus a
		/// number — the hash either ends the text before it, or stands alone in a panel of its own,
		/// where the length rule leaves it untouched.</remarks>
		public const string Hash = "#";
		public const string Volume = "vol";
		public const string SeekTitle = "Seek within this song";
		/// <summary>Playback is stalled on a song being rendered — as opposed to the silent
		/// background look-ahead, which nobody needs to be told about.</summary>
		public static string Generating( int n ) => $"generating #{n}…";

		// Seed
		public const string SeedPlaceholder = "tag:n[:genre][:vibe]";
		/// <summary>Typing a seed and being handed a stopped transport is a dead end, so the button
		/// says play, because that is what it does.</summary>
		public const string SeedGo = "play";
		public const string CopySong = "copy seed";
		public const string CopySongTitle = "Copy this song fully written down — the genre and the vibe spelled out, so it plays the same anywhere";
		public const string CopyStation = "copy station";
		public const string CopyStationTitle = "Copy the seed as it stands — whatever it leaves rolling keeps rolling";
		public const string Copied = "copied!";

		// What plays
		public const string Genre = "genre";
		public const string GenreRandom = "Random";
		public const string Reroll = "🎲 reroll";
		public const string RerollTitle = "A fresh station at song 0 — anything you have pinned stays pinned";
		public const string ShuffleOn = "🔀 shuffle: ON";
		public const string ShuffleOff = "🔀 shuffle: OFF";
		public const string ShuffleTitle = "Every next song is a whole new station rather than the next song of this one";
		public const string Tinker = "🎛 tinker";
		public const string TinkerOpen = "hide knobs";

		// The knobs
		public const string VibeHeading = "vibe";
		public const string GlobalHeading = "GLOBAL";
		public const string VibeRoll = "🎲 randomize";
		public const string VibeRollTitle = "Throw every knob somewhere new and keep it — the seed carries these values";
		public const string VibeRandom = "↺ random each song";
		public const string VibeRandomTitle = "Stop pinning these knobs — let every song roll its own again";

		// The playlist
		public const string PlaylistHeading = "playlist";
		public const string JumpTo = "jump to";
		public const string JumpGo = "go";
		public const string RowNow = "now";
		public const string RowReady = "ready";
		public const string RowGone = "gone";
		public const string RowPending = "—";
		public const string Export = "⬇ .wav";
		public const string ExportBusy = "⬇ …";
		public static string ExportTitle( int n ) => $"Export #{n}";

		// What the message line says. A refused seed says so INLINE and changes nothing: a toast
		// that has faded is no help to somebody looking at a board that did nothing.
		public static string Saved( string file ) => $"Saved {file} to your s&box data folder";
		public const string SaveFailed = "Couldn't save song";
		public static string Playing( string seed ) => $"Playing {seed}";
		public const string NewStation = "New station";
		public const string VibeRolled = "Threw every knob but the volumes, and pinned them";
		public const string VibeUnpinned = "Every song rolls its own vibe again — what changes is the songs after this one";
		public const string GenreUnpinned = "Every song rolls its own genre again";
	}
}
gamah.skafinity / Code/Engine/Drums/Kit.cs
Game library
using System;
using System.Collections.Generic;

using static Skafinity.Osc;

namespace Skafinity;

// The kit's voices: synthesised kick, snare, tom, hat, crash and ride.
//
// Each one returns immediately when the kit is muted (_drumGain 0) — it would write silence.
// The guard sits INSIDE the voices rather than at the call site because the caller interleaves
// pattern decisions with these calls: RenderDrumBar draws noise.Chance() to pick tom-vs-ghost
// and RenderFill draws rng.Chance() between hits, so skipping a call would move the stream.
// The per-voice `noise` draws being skipped are local — `noise` is a fresh per-block Rng, and
// when the kit is muted nothing downstream reads it.
//
// EVERY VOICE IS PARAMETERISED BY A TONE STRUCT, and every struct's Default reproduces the
// numbers the groove path has always played. A candidate tuning is an argument, not an edit, so
// the audition diagnostic can sweep one without the grooves hearing about it — which is what
// lets a kit be chosen by listening rather than by rebuilding between takes.
//
// Part of the MusicGen engine — see MusicGen.cs.

/// <summary>Two-pole band-pass (Chamberlin SVF), for the cymbal voices' resonant clusters. The
/// synth's own SVF is inline in the pitched render loop (Synth/Render.cs) and is not reachable
/// from here; this is the same filter, kept next to the voices that use it.</summary>
struct BandPass
{
	float _low, _band;
	readonly float _f, _q;

	public BandPass( float fc, float q, int sr )
	{
		_low = 0f; _band = 0f;
		// Clamped well under Nyquist: the Chamberlin form goes unstable as f approaches 2.
		_f = (float)(2 * Math.Sin( Math.PI * Math.Min( fc, sr * 0.15f ) / sr ));
		_q = Math.Clamp( q, 0.004f, 2f );
	}

	/// <summary>NORMALISED BY Q. A resonant band-pass has a centre gain of about 1/Q, so a
	/// ringing resonance is ~100× louder than a gentle one for the same input — which makes the
	/// Q a level control as well as a bandwidth, and there is no setting of the two that is then
	/// independently correct. Scaling by Q on the way out separates them again: how tight the
	/// resonance is, and how loud it is, become two numbers.</summary>
	public float Next( float x )
	{
		float high = x - _low - _q * _band;
		_band += _f * high;
		_low += _f * _band;
		return _band * _q;
	}
}

/// <summary>
/// WHAT THE AUDITION APPROVED, and it is mostly RANGES rather than values.
///
/// Several of round 2's questions came back "all of these work" — the click's corner at 1.8, 3.5
/// and 6 kHz; the rimshot's crack at 2.4, 3.2 and 4.2 kHz; both cross-sticks; both foot chicks.
/// That is not an undecided answer. A kit is a physical object being hit by a person, and the
/// same drum does not make the identical sound twice; a band of values that all read as the
/// right drum is exactly what NUANCE is, and picking one point out of it by ear would be
/// throwing the finding away. Phase 2 draws from these per song (and per hit where it says so),
/// which is the same reason RenderKick's round-robin jitter exists.
///
/// NOTHING HERE IS WIRED IN YET. The tone structs' Defaults are still what the grooves play, so
/// the render digests are untouched and Phase 1 stays provably pure. These are the numbers
/// Phase 2 wires, and they are recorded here rather than in a comment so they cannot drift.
/// </summary>
static class KitNuance
{
	/// <summary>Kick click low-pass corner. Full-band was the high tick on every body.</summary>
	public const float ClickCutMin = 1800f, ClickCutMax = 6000f;

	/// <summary>Rimshot crack centre. Darker reads as a bigger drum, brighter as a harder hit —
	/// both are the same articulation, so this is per HIT, not per song.</summary>
	public const float RimCrackMin = 2400f, RimCrackMax = 4200f;

	/// <summary>Cross-stick, between the two that were kept: the higher/thinner knock and the
	/// lower/thicker one. Crack centre and thud level move together.</summary>
	public const float StickCrackMin = 1350f, StickCrackMax = 1750f;
	public const float StickThudMin = 0.34f, StickThudMax = 0.50f;

	/// <summary>Foot chick, between the two that were kept: the tighter, brighter chick and the
	/// slower, darker one. All three move together — a foot that closes slower is duller and
	/// longer, because it is the same motion done differently.</summary>
	public const float FootAttackMin = 0.011f, FootAttackMax = 0.022f;
	public const float FootDurMin = 0.085f, FootDurMax = 0.115f;
	public const float FootCutMin = 2600f, FootCutMax = 3400f;

	/// <summary>The open hi-hat's ring, and its corner. Approved as a range for the same reason
	/// the others are: the tail of an open hat is how hard the foot was off it, which is a
	/// different amount every bar. A hat that rings the identical 600 ms eight times is the same
	/// tell as a kick that never varies.
	///
	/// <para>The band was 0.45–0.75 s, and that is a hat measured on its own rather than one in a
	/// pattern. With <c>decayFrac</c> ~0.45 it is a 200–340 ms time constant, while an eighth at
	/// the top of a genre's band is ~185 ms — so the open hat was still half up when the next
	/// stroke landed, on every open cell, and a groove that plays them continuously (the country
	/// train beat) reads as a wash that never clears rather than as an open hat. The range is kept
	/// because the nuance is real; it is the LENGTH that was a solo measurement.</para>
	///
	/// <para>THE VALUE HERE IS THE ONE THAT COUNTS: <c>HatTone.Default.openDur</c> is overridden
	/// per song from this band (see Compose.cs), so editing the preset moves nothing a listener
	/// hears — the digests not budging is what says so.</para></summary>
	public const float OpenHatDurMin = 0.26f, OpenHatDurMax = 0.42f;
	public const float OpenHatCut = 6250f;

	/// <summary>Where half open sits. The pedal's travel is geometric (see RenderHat), and this is
	/// the exponent on it: 1 is a straight ratio sweep, below 1 opens the low end of the travel
	/// sooner. A steady lift has to sound steady, which is the thing this number is set against —
	/// a single half-open hat cannot tell you whether the map is even, only whether one point on
	/// it is pleasant.</summary>
	public const float HatOpenCurve = 0.50f;

	/// <summary>A LIFT LANDS ON A CHOKE. The foot comes down on the downbeat and whatever is
	/// ringing stops; that is the event, and it reads correctly whether or not a stick lands with
	/// it. What does NOT work is a foot chick as the landing — the chick is its own articulation
	/// (it is the hat speaking on its own, on 2 and 4, under silence) and it has nothing to say
	/// at the end of a phrase that a choke has not already said.</summary>
	/// <summary>THE CYMBALS' BANDS. Carried over from the audition rounds that swept them on the
	/// mode-forest cymbal: they are parameters of the same three laws — how much splash, how much
	/// wash, how long the ring, where the bell's clang sits — so they transfer, but they were
	/// approved on a different spelling of those laws and want a fresh listen. Only what was
	/// actually swept is a band: the bell's splash and the bright crash's wash were never varied in
	/// front of a listener and stay at 1.</summary>
	public const float RideSplashMin = 0.5f, RideSplashMax = 1.8f;
	public const float RideWashMin = 1.0f, RideWashMax = 1.8f;
	public const float RideRingMin = 1.0f, RideRingMax = 1.4f;
	public const float BellClangMin = 2000f, BellClangMax = 2600f;
	public const float BellRingMin = 1.0f, BellRingMax = 1.4f;
	public const float CrashSplashMin = 0.5f, CrashSplashMax = 1.6f;
	public const float CrashRingMin = 0.7f, CrashRingMax = 1.4f;
	public const float DarkSplashMin = 0.5f, DarkSplashMax = 1.0f;
	public const float DarkWashMin = 1.0f, DarkWashMax = 1.6f;
	public const float DarkRingMin = 1.0f, DarkRingMax = 1.3f;

	/// <summary>Interpolate a kit nuance. <paramref name="u"/> is 0..1 — a per-song or per-hit
	/// draw. One helper so a nuance is always read the same way.</summary>
	public static float At( float min, float max, float u )
		=> min + (max - min) * Math.Clamp( u, 0f, 1f );
}

// ── The tone structs ──

/// <summary>The kick's body. Defaults are what the grooves have always played.</summary>
readonly struct KickTone
{
	public readonly float Dur;          // seconds
	public readonly double DecayFrac;   // decay time as a fraction of Dur
	public readonly float StartHz;      // pitch at the attack
	public readonly float DropHz;       // how far the pitch falls
	public readonly float DropRate;     // how fast it falls (in units of 1/Dur)
	public readonly float Drive;        // tanh drive on the body
	public readonly float SubHz;
	public readonly float SubLevel;
	public readonly double SubDecayFrac;
	public readonly float ClickLevel;
	public readonly float ClickSec;
	/// <summary>Low-pass corner on the click. 0 leaves it full-band — which is white noise, and
	/// reads as a high tick sitting on top of the drum rather than as a beater hitting a head.
	/// A beater is a soft mass on a skin: the attack it makes is mid-band.</summary>
	public readonly float ClickCut;
	public readonly float Beater;       // level of the beater-return transient (0 = none)
	public readonly float BeaterSec;    // how long after the hit the beater comes back
	public readonly float Jitter;       // round-robin variation depth, 0..1

	public KickTone( float dur, double decayFrac, float startHz, float dropHz, float dropRate,
		float drive, float subHz, float subLevel, double subDecayFrac, float clickLevel,
		float clickSec, float clickCut, float beater, float beaterSec, float jitter )
	{
		Dur = dur; DecayFrac = decayFrac; StartHz = startHz; DropHz = dropHz; DropRate = dropRate;
		Drive = drive; SubHz = subHz; SubLevel = subLevel; SubDecayFrac = subDecayFrac;
		ClickLevel = clickLevel; ClickSec = clickSec; ClickCut = clickCut;
		Beater = beater; BeaterSec = beaterSec;
		Jitter = jitter;
	}

	public KickTone With( float dur = -1f, double decayFrac = -1.0, float startHz = -1f,
		float dropHz = -1f, float dropRate = -1f, float drive = -1f, float subHz = -1f,
		float subLevel = -1f, double subDecayFrac = -1.0, float clickLevel = -1f,
		float clickSec = -1f, float clickCut = -1f, float beater = -1f, float jitter = -1f )
		=> new( dur < 0 ? Dur : dur, decayFrac < 0 ? DecayFrac : decayFrac,
			startHz < 0 ? StartHz : startHz, dropHz < 0 ? DropHz : dropHz,
			dropRate < 0 ? DropRate : dropRate, drive < 0 ? Drive : drive,
			subHz < 0 ? SubHz : subHz, subLevel < 0 ? SubLevel : subLevel,
			subDecayFrac < 0 ? SubDecayFrac : subDecayFrac,
			clickLevel < 0 ? ClickLevel : clickLevel, clickSec < 0 ? ClickSec : clickSec,
			clickCut < 0 ? ClickCut : clickCut,
			beater < 0 ? Beater : beater, BeaterSec, jitter < 0 ? Jitter : jitter );

	public static readonly KickTone Default = new(
		dur: 0.17f, decayFrac: 0.31, startHz: 127f, dropHz: 80f, dropRate: 2.6f, drive: 1.6f,
		subHz: 44f, subLevel: 0.3f, subDecayFrac: 0.55, clickLevel: 0.55f, clickSec: 0.003f,
		clickCut: 0f, beater: 0f, beaterSec: 0.023f, jitter: 0f );
}

/// <summary>How the snare is struck. The single-hit articulations are tone presets; the flam and
/// the buzz are gestures made of several hits and have their own entry points.</summary>
enum SnareHit { Hit, Ghost, Rimshot, CrossStick, SnaresOff }

readonly struct SnareTone
{
	public readonly float Dur;
	public readonly double DecayFrac;
	public readonly float Hz1, Hz2;
	public readonly float Body2;      // level of the second shell partial
	public readonly float BodyLevel;
	public readonly float Sag;        // how far the shell pitch falls over the hit
	public readonly float Wire;       // amount of snare-wire noise
	public readonly float WireCut;    // wire high-pass corner
	public readonly float WireDrive;
	/// <summary>How hard this articulation is struck, relative to a plain backbeat.</summary>
	public readonly float Level;
	/// <summary>THE CRACK: a tight, fast band of noise at the attack — stick on rim, wood on wood.
	/// It is what a rimshot and a cross-stick actually ARE. Reaching for them with the shell
	/// partials instead is what makes a rimshot ring like a tom and a cross-stick read as a clave:
	/// two loud sines with a slow decay are a pitched percussion instrument, whatever they are
	/// labelled. 0 = no crack, which is the plain backbeat.</summary>
	public readonly float CrackHz, CrackQ, CrackLevel;
	public readonly double CrackDecayFrac;
	/// <summary>A little low knock under the crack — the shell moving. Not a tone.</summary>
	public readonly float ThudHz, ThudLevel;

	public SnareTone( float dur, double decayFrac, float hz1, float hz2, float body2,
		float bodyLevel, float sag, float wire, float wireCut, float wireDrive, float level,
		float crackHz = 0f, float crackQ = 0.35f, float crackLevel = 0f,
		double crackDecayFrac = 0.10, float thudHz = 0f, float thudLevel = 0f )
	{
		Dur = dur; DecayFrac = decayFrac; Hz1 = hz1; Hz2 = hz2; Body2 = body2;
		BodyLevel = bodyLevel; Sag = sag; Wire = wire; WireCut = wireCut; WireDrive = wireDrive;
		Level = level;
		CrackHz = crackHz; CrackQ = crackQ; CrackLevel = crackLevel;
		CrackDecayFrac = crackDecayFrac; ThudHz = thudHz; ThudLevel = thudLevel;
	}

	public SnareTone With( float dur = -1f, double decayFrac = -1.0, float hz1 = -1f, float hz2 = -1f,
		float bodyLevel = -1f, float sag = -1f, float wire = -1f, float wireCut = -1f,
		float level = -1f, float crackHz = -1f, float crackLevel = -1f, float thudLevel = -1f )
		=> new( dur < 0 ? Dur : dur, decayFrac < 0 ? DecayFrac : decayFrac,
			hz1 < 0 ? Hz1 : hz1, hz2 < 0 ? Hz2 : hz2, Body2,
			bodyLevel < 0 ? BodyLevel : bodyLevel, sag < 0 ? Sag : sag,
			wire < 0 ? Wire : wire, wireCut < 0 ? WireCut : wireCut, WireDrive,
			level < 0 ? Level : level, crackHz < 0 ? CrackHz : crackHz, CrackQ,
			crackLevel < 0 ? CrackLevel : crackLevel, CrackDecayFrac, ThudHz,
			thudLevel < 0 ? ThudLevel : thudLevel );

	public static readonly SnareTone Default = new(
		dur: 0.15f, decayFrac: 0.32, hz1: 185f, hz2: 268f, body2: 0.6f, bodyLevel: 0.375f,
		sag: 0.14f, wire: 0.6f, wireCut: 1350f, wireDrive: 1.2f, level: 1f );

	/// <summary>The ghost note — the groove path's `ghost: true`, byte for byte.</summary>
	public static readonly SnareTone Ghost = new(
		dur: 0.06f, decayFrac: 0.3, hz1: 185f, hz2: 268f, body2: 0.6f, bodyLevel: 0.375f,
		sag: 0.14f, wire: 0.6f, wireCut: 1350f, wireDrive: 1.2f, level: 0.3f );

	/// <summary>Stick on rim and head together: the shell speaks louder and higher, the wires
	/// crack harder, and the whole thing is shorter than a struck note.</summary>
	public static readonly SnareTone Rimshot = new(
		dur: 0.14f, decayFrac: 0.20, hz1: 300f, hz2: 452f, body2: 0.5f, bodyLevel: 0.13f,
		sag: 0.26f, wire: 0.95f, wireCut: 1900f, wireDrive: 2.6f, level: 1.25f,
		crackHz: 3200f, crackQ: 0.55f, crackLevel: 2.2f, crackDecayFrac: 0.055 );

	/// <summary>Stick laid across the head, struck on the rim: a woody KNOCK. It is damped by the
	/// hand holding the stick down, so it does not ring — and it is the ringing, not the pitch,
	/// that makes a bright short tone read as a clave.</summary>
	public static readonly SnareTone CrossStick = new(
		dur: 0.05f, decayFrac: 0.085, hz1: 520f, hz2: 735f, body2: 0.45f, bodyLevel: 0.24f,
		sag: 0.30f, wire: 0.07f, wireCut: 2400f, wireDrive: 1.0f, level: 0.85f,
		crackHz: 1750f, crackQ: 0.75f, crackLevel: 1.3f, crackDecayFrac: 0.10,
		thudHz: 155f, thudLevel: 0.34f );

	/// <summary>Wires thrown off: no crack, just the shell — which is what makes it read as a
	/// tom rather than as a quiet snare.</summary>
	public static readonly SnareTone SnaresOff = new(
		dur: 0.22f, decayFrac: 0.30, hz1: 178f, hz2: 253f, body2: 0.62f, bodyLevel: 0.52f,
		sag: 0.22f, wire: 0.04f, wireCut: 1350f, wireDrive: 1.0f, level: 1f );

	public static SnareTone For( SnareHit h ) => h switch
	{
		SnareHit.Ghost => Ghost,
		SnareHit.Rimshot => Rimshot,
		SnareHit.CrossStick => CrossStick,
		SnareHit.SnaresOff => SnaresOff,
		_ => Default,
	};
}

/// <summary>How a three-piece tom set is tuned. The interval is the character; which pitch it
/// starts from comes from the song's key.</summary>
enum TomTune
{
	/// <summary>Two stacked perfect fourths — the conventional tuning, and the one whose fills
	/// read as a descending scale rather than as a slide.</summary>
	Fourths,
	/// <summary>Fifths: a wider spread, so each drum is unmistakably its own drum.</summary>
	Wide,
	/// <summary>Stacked major thirds — close-tuned, so a fill reads as one gesture across a kit
	/// rather than as three separate notes.</summary>
	Thirds,
	/// <summary>A fixed physical set that ignores the key entirely. A drummer does not retune
	/// between songs, and this is the candidate that says so.</summary>
	Fixed,
}

/// <summary>THE KIT, not a pitch: three tom pitches in fixed positions, addressed by INDEX.
///
/// 0 is the rack tom (highest), 2 the floor (lowest). Everything downstream takes the index, so
/// a fill's position across the stereo field comes from which drum was hit — the old map from a
/// frequency onto a hardcoded 145–260 Hz range could be, and was, driven off its own bottom end
/// by the fills that used it. Same trick as Register(octaves): the wrong version is unwriteable.
/// </summary>
readonly struct TomKit
{
	public const int Count = 3;

	readonly float _f0, _f1, _f2;
	public readonly bool RackLeft;

	public TomKit( float f0, float f1, float f2, bool rackLeft = true )
	{
		_f0 = f0; _f1 = f1; _f2 = f2; RackLeft = rackLeft;
	}

	public float Hz( int i ) => i <= 0 ? _f0 : i == 1 ? _f1 : _f2;

	/// <summary>Where this drum sits, −1 hard left … +1 hard right, before the kit's own spread.
	/// The caller scales it (the drums' stereo width is one number and lives outside the kit).
	/// </summary>
	public float Pan( int i )
	{
		float u = Math.Clamp( i, 0, Count - 1 ) / (Count - 1f);   // 0 rack … 1 floor
		return (RackLeft ? 1f : -1f) * (u * 2f - 1f);
	}

	/// <summary>The tuning for a song in this key. The key sets WHICH pitch the set starts on;
	/// the shape sets the intervals. Only the pitch CLASS is read, so the set stays inside a
	/// drum-sized range whatever octave the song is written in — and it is drawn from the song's
	/// root rather than a section's key shift, so toms cannot drift mid-song.</summary>
	public static TomKit Tuned( TomTune shape, int rootMidi, bool rackLeft = true )
	{
		if ( shape == TomTune.Fixed ) return new TomKit( 196f, 147f, 110f, rackLeft );
		int pc = ((rootMidi % 12) + 12) % 12;
		int floorMidi = 41 + pc;                       // 87 .. 165 Hz — a floor tom's range
		int step = shape switch { TomTune.Wide => 7, TomTune.Thirds => 4, _ => 5 };
		return new TomKit( Midi( floorMidi + 2 * step ), Midi( floorMidi + step ),
			Midi( floorMidi ), rackLeft );
	}
}

readonly struct TomTone
{
	public readonly float Dur;
	public readonly double DecayFrac;
	public readonly float Sag;            // how far the head's pitch falls over the hit
	public readonly float SnapMul;        // the inharmonic upper partial, as a ratio
	public readonly float SnapLevel;
	public readonly double SnapDecayFrac;
	public readonly float ClickLevel;     // stick attack
	public readonly float ClickSec;

	public TomTone( float dur, double decayFrac, float sag, float snapMul, float snapLevel,
		double snapDecayFrac, float clickLevel, float clickSec )
	{
		Dur = dur; DecayFrac = decayFrac; Sag = sag; SnapMul = snapMul; SnapLevel = snapLevel;
		SnapDecayFrac = snapDecayFrac; ClickLevel = clickLevel; ClickSec = clickSec;
	}

	public TomTone With( float dur = -1f, double decayFrac = -1.0, float sag = -1f,
		float snapLevel = -1f, float clickLevel = -1f )
		=> new( dur < 0 ? Dur : dur, decayFrac < 0 ? DecayFrac : decayFrac, sag < 0 ? Sag : sag,
			SnapMul, snapLevel < 0 ? SnapLevel : snapLevel, SnapDecayFrac,
			clickLevel < 0 ? ClickLevel : clickLevel, ClickSec );

	public static readonly TomTone Default = new(
		dur: 0.18f, decayFrac: 0.3, sag: 0.22f, snapMul: 2.5f, snapLevel: 0.5f,
		snapDecayFrac: 0.06, clickLevel: 0.45f, clickSec: 0.006f );
}

/// <summary>What the hi-hat does. Openness is a continuum and lives outside this — these are the
/// articulations that are not simply "how far open".</summary>
enum HatHit { Stick, Foot, Splash }

readonly struct HatTone
{
	public readonly float ClosedDur, OpenDur;
	public readonly double DecayFrac;
	public readonly float ClosedCut, OpenCut;
	public readonly float Level;
	public readonly float LowThud;      // the foot's pedal-board thump; 0 for a stick hit
	/// <summary>An ATTACK RAMP. A stick hit starts instantly; a foot chick does not — the cymbals
	/// travel together and the sound arrives over a few milliseconds. That ramp is the difference
	/// between "shhck" and a quiet closed hit, and no amount of filtering substitutes for it.</summary>
	public readonly float AttackSec;
	/// <summary>The curve openness travels on. Linear puts half-open half way between a 35 ms tick
	/// and an open hat, which is nowhere near half way in what is HEARD — the ear reads a ratio,
	/// not a difference, so the middle of a linear map is still a closed hat.</summary>
	public readonly float OpenCurve;
	/// <summary>Loose cymbals rattling against each other. It peaks at half open, because that is
	/// the only place two cymbals are touching AND free to move.</summary>
	public readonly float SizzleHz, SizzleDepth;

	public HatTone( float closedDur, float openDur, double decayFrac, float closedCut,
		float openCut, float level, float lowThud, float attackSec = 0f, float openCurve = 1f,
		float sizzleHz = 0f, float sizzleDepth = 0f )
	{
		ClosedDur = closedDur; OpenDur = openDur; DecayFrac = decayFrac; ClosedCut = closedCut;
		OpenCut = openCut; Level = level; LowThud = lowThud;
		AttackSec = attackSec; OpenCurve = openCurve; SizzleHz = sizzleHz; SizzleDepth = sizzleDepth;
	}

	public HatTone With( float closedDur = -1f, float openDur = -1f, double decayFrac = -1.0,
		float closedCut = -1f, float openCut = -1f, float level = -1f, float attackSec = -1f,
		float openCurve = -1f, float sizzleHz = -1f, float sizzleDepth = -1f )
		=> new( closedDur < 0 ? ClosedDur : closedDur, openDur < 0 ? OpenDur : openDur,
			decayFrac < 0 ? DecayFrac : decayFrac, closedCut < 0 ? ClosedCut : closedCut,
			openCut < 0 ? OpenCut : openCut, level < 0 ? Level : level, LowThud,
			attackSec < 0 ? AttackSec : attackSec, openCurve < 0 ? OpenCurve : openCurve,
			sizzleHz < 0 ? SizzleHz : sizzleHz, sizzleDepth < 0 ? SizzleDepth : sizzleDepth );

	/// <remarks>openDur here is only the base a song varies FROM: Compose.cs overrides it out of
	/// KitNuance.OpenHatDurMin/Max on every song, so this number reaches nothing but the audition
	/// path. Change the band, not this.</remarks>
	public static readonly HatTone Default = new(
		closedDur: 0.035f, openDur: 0.16f, decayFrac: 0.4, closedCut: 7000f, openCut: 7000f,
		level: 1f, lowThud: 0f );

	/// <summary>The foot chick: the pedal closing the cymbals with no stick involved. Duller
	/// than a struck closed hat and carrying the board's own thump.</summary>
	public static readonly HatTone Foot = new(
		closedDur: 0.085f, openDur: 0.085f, decayFrac: 0.34, closedCut: 3400f, openCut: 3400f,
		level: 0.9f, lowThud: 0.16f, attackSec: 0.011f );

	/// <summary>Foot splash: opened and closed again in one motion — a short open hat with a
	/// bright top and no tail to speak of.</summary>
	public static readonly HatTone Splash = new(
		closedDur: 0.26f, openDur: 0.26f, decayFrac: 0.30, closedCut: 8200f, openCut: 8200f,
		level: 0.85f, lowThud: 0.10f );

	public static HatTone For( HatHit h ) => h switch
	{
		HatHit.Foot => Foot,
		HatHit.Splash => Splash,
		_ => Default,
	};
}

/// <summary>
/// THE CYMBAL, DISTILLED — a measured spectrum spent on thirteen components instead of four
/// hundred.
///
/// Real cymbals were measured for this (provenance below) and the measurement collapsed into three
/// laws. The first attempt spent them on a MODE FOREST: ~390 resolved partials for the ride, each
/// with its own ring time. It was accurate, and it was wrong twice over — it cost ~250 ms of CPU a
/// hit, and it out-detailed every other voice in the engine by two orders of magnitude. The rest of
/// this kit is two or three sines and some filtered noise; a cymbal built to a different standard
/// does not sit in that mix at any level, because the problem is not that it is loud. So the laws
/// are kept and the spelling is not:
///
///   * <b>τ·√f constant</b> → <b>PER-BAND DECAY</b>. Seven noise bands whose ring times fall as
///     1/√f. This is the whole of what says "struck metal" and it is seven numbers.
///   * <b>a mode forest at constant density</b> → <b>BAND-LIMITED NOISE</b>. Density the ear cannot
///     resolve into partials IS noise; four hundred resonators were an expensive way to spell it.
///   * <b>beating near-pairs</b> → <b>ONE LOW PAIR</b> of real partials, quiet, down where the ear
///     resolves the beat and where a cymbal's size is heard.
///   * <b>strike position as a log-Gaussian bump</b> → <b>the band gains</b>, one evaluation each.
///   * splash and wash ride underneath, as they always did.
///
/// THIS IS NOT GENERATION 2, AND THE DIFFERENCE IS ONE PROPERTY. Lightly band-passed noise was
/// tried early and came back "hats in weird states" — correct, and the cause was that it had ONE
/// decay for the whole voice. A noise band that dies uniformly is a hat. Bands whose ring times
/// diverge by a factor of five across the spectrum are a cymbal, and that divergence is measured
/// rather than dialled. Equally it is not generation 5, which made the cymbal out of pure
/// waveforms and produced church bells every time: the only tonal components here are one quiet
/// low pair, and everything above them is noise. The two failures bracket the target — uniform
/// decay is a hat, resolvable partials are a bell — and per-band decay is what sits between them.
///
/// Provenance (NOT vendored, and must not become a dependency — what lands is these constants and
/// this citation): Virtuosity Drums by Versilian Studios &amp; Karoryfer Samples,
/// github.com/sfzinstruments/virtuosity_drums, CC0-1.0. Measured 2026-08-02 from the overhead-mic
/// samples — oh_ride_ride_vl3 (bow), oh_ride_bell_vl3 (bell), oh_crash_crash_vl3 (bright crash),
/// oh_flatride_crash_vl4 (dark crash). Sustained partials from a 131072-point FFT starting 0.33 s
/// after onset; per-band ring times from exponential fits over a 4096/1024 STFT. tools/spectool is
/// the reader and stays in the repo, so every number here can be re-derived — and --cymbal writes
/// one dry hit per cymbal to feed it, because a spectrum fitted to a measurement is not fitted
/// until the RESULT has been measured the same way.
/// </summary>
readonly struct CymbalBands
{
	/// <summary>The noise bands: centre, gain, and the ring time that band decays with.</summary>
	public readonly float[] Hz, Amp, Tau;
	public readonly float Dur, Level, Stick, StickCut;
	public readonly float SplashLvl, SplashTau, WashLvl, WashTau, NoiseHp, WashLp;
	/// <summary>Where the SPLASH starts, separately from the wash. A crash's attack is measured
	/// broadband and stays that way; a ride's is a stick touching metal, which is a high-frequency
	/// event — and it is the only part of a ride that lands in a band the rest of the arrangement
	/// leaves empty. One shared corner meant every attempt to make the stroke cut also added to
	/// the mids, where it is masked and does nothing but thicken.</summary>
	public readonly float SplashHp;

	CymbalBands( float[] hz, float[] amp, float[] tau, float dur, float level, float stick,
		float stickCut, float splashLvl, float splashTau, float washLvl, float washTau,
		float noiseHp, float washLp, float splashHp )
	{
		Hz = hz; Amp = amp; Tau = tau; Dur = dur; Level = level; Stick = stick; StickCut = stickCut;
		SplashLvl = splashLvl; SplashTau = splashTau; WashLvl = washLvl; WashTau = washTau;
		NoiseHp = noiseHp; WashLp = washLp; SplashHp = splashHp;
	}

	// ── The laws ──

	/// <summary>LAW 1 — the ring, and it is the law that is NOT shared between cymbals. On the ride
	/// every per-band fit lands on τ ≈ 39/√f within take-to-take scatter (230 Hz → 2.6 s, 850 → 1.3,
	/// 2.7 k → 0.75, 5.7 k → 0.51), and the long ring is the instrument. Both crashes measured a
	/// different shape and each needs one extra term:
	///
	///  * <paramref name="knee"/> — a LOW CUT. The bright crash holds τ·√f ≈ 45 only above ~1 kHz;
	///    below that its lows die fast (0.9 s at 375 Hz where the bare law says 2.0). A big thin
	///    plate struck hard dumps its low modes into the room at once.
	///  * <paramref name="sizzle"/> — a rising FLOOR. The dark crash inverts the ride: low-mids gone
	///    in half a second while 7–14 kHz rings for 1.5–2.0 s. Wash and rivet behaviour rather than
	///    plate behaviour, so it is a second term taking over where it is the longer of the two.
	/// </summary>
	static float RingTau( float hz, float k, float knee, float sizzle )
	{
		float t = k / MathF.Sqrt( hz );
		if ( knee > 0f && hz < knee ) t *= hz / knee;
		if ( sizzle > 0f ) t = MathF.Max( t, sizzle * MathF.Sqrt( hz / SizzleRef ) );
		return t;
	}
	const float SizzleRef = 8000f;   // where the sizzle term is quoted: dark crash τ ≈ 1.5 s there

	/// <summary>LAW 2 — the band set. The forest's density said the partials are unresolvable, so
	/// what matters is only how the energy and the ring vary ACROSS frequency: seven bands,
	/// geometrically spaced, is enough resolution for a curve that changes by a factor of five over
	/// the whole range. The top must reach ~12 kHz — the measured sustain holds −9…−14 dB at
	/// 5–10 kHz ringing at ~0.5 s, and an earlier version cut off at 4 kHz and measured 10–15 dB
	/// light against the reference.</summary>
	/// <summary>NO TONAL COMPONENTS AT ALL, and the band set reaches down instead. An earlier pass
	/// kept one low PAIR of real partials for the measured beating — two sines a few Hz apart, on
	/// the argument that a beat is not a pitch. It is a pitch: 232 Hz ringing for two and a half
	/// seconds under noise bands that decay much faster is the most exposed thing in the voice, and
	/// it read as a sine sitting inside every cymbal. That is generation 5's church bell arriving
	/// through a side door, at a lower level and with a better excuse. The bottom band carries the
	/// cymbal's size instead, as noise, which is what the rest of the voice is made of.</summary>
	const int Bands = 8;
	const float BandLo = 180f, BandHi = 12000f;
	/// <summary>Band width, as the filter's damping. Wide enough that the filter itself does not
	/// ring — its own decay is under 2 ms at the bottom band — because the DECAY here is the
	/// envelope's job. A resonance that rings is a partial, and partials are what generation 5
	/// proved a cymbal must not be made of.</summary>
	const float BandQ = 0.7f;

	/// <summary>LAW 3 — a strike position is a spectral bump on a log axis. The bow is one wide
	/// bump (the measured sustain is flat 300 Hz–3.2 kHz with soft edges); the bell is a narrow
	/// clang plus a small low knock where the stick shocks the cup; the bright crash centres at
	/// 2.2–4.7 kHz over a low knock; the dark crash is low-heavy with a sizzle top that has to be
	/// excited before the ring law can let it outlive anything.</summary>
	static float LogBump( float f, float centre, float width )
	{
		float u = MathF.Log( f / centre ) / width;
		return MathF.Exp( -0.5f * u * u );
	}

	readonly struct Strike
	{
		public readonly float Centre, Width, Centre2, Width2, Level2;
		public Strike( float centre, float width, float centre2 = 0f, float width2 = 0.3f,
			float level2 = 0f )
		{
			Centre = centre; Width = width;
			Centre2 = centre2; Width2 = width2; Level2 = level2;
		}
		public float Weight( float f )
			=> LogBump( f, Centre, Width ) + (Level2 > 0f ? Level2 * LogBump( f, Centre2, Width2 ) : 0f);
	}

	// THE BOW'S BUMP IS A MIX DECISION AND NOT THE MEASUREMENT. The reference puts a real ride's
	// sustain at ~1.2 kHz, and that is where this sat — which is both darker than the hi-hat it
	// stands in for and, worse, exactly where the guitars are. Measured against an open hat as
	// spectral centroid: the hat is 12.5 kHz at the attack and STILL 12.4 kHz half a second later,
	// because it is one high-passed noise with one decay; the ride was 10.4 kHz falling to 8.8 kHz,
	// because tau=k/sqrt(f) means the low bands outlive the high ones and a cymbal built on that law
	// ALWAYS darkens as it rings. So a ride reads as the dark voice against a hat that never moves,
	// which is backwards for the one carrying the pulse. At 4200 Hz with the noise floor lifted the
	// tail comes to 10.9 kHz — still under the hat, and 2.1 kHz brighter than it was.
	static readonly Strike BowStrike = new( 4200f, 0.95f );
	static Strike BellStrike( float clang ) => new( clang, 0.45f, 290f, 0.25f, 0.30f );
	static readonly Strike BrightCrashStrike = new( 3200f, 0.80f, 400f, 0.55f, 0.25f );
	static readonly Strike DarkCrashStrike = new( 520f, 0.75f, 9000f, 0.60f, 0.45f );

	/// <summary>How loud one STROKE is, and it is not the same for a ride and a crash even though
	/// the same arm strikes both. A ride stroke rings for seconds and is played eight times a bar,
	/// so a riding section has a dozen rings sounding at once; the hi-hat it replaces is 35 ms and
	/// never overlaps itself. Level per stroke and level in the mix are two different quantities —
	/// measured with --levels, a ride at the crash's stroke level put country's whole kit 2.6 dB
	/// over the rest of its band on its riding sections alone. A crash overlaps nothing, being one
	/// gesture a phrase, so it keeps the louder stroke.</summary>
	// StrokeLevelRide was 0.30 and went to 0.95 in one +10 dB step, by ear, to stop the ride being
	// buried — and that commit's own message flagged the result as "worth watching". This is that
	// watch firing: measured in the >2.5 kHz band, where nothing else in a ska arrangement lives,
	// 0.95 puts the ride +6.9 dB over the ENTIRE REST OF THE MIX and holds that band within 12 dB
	// of its own peak 43% of the time. 0.30 was +1.4 dB and 12.8%, which is the buried the boost
	// was aimed at; 0.55 was +3.65 dB and 21.3% — present without being the arrangement. IT IS 0.45
	// NOW and those two figures describe the 0.55 it was: the bow's strike bump moved to 4200 Hz
	// and the level came down with it, which on the song read +4.74 -> +1.98 dB at 1-3 kHz against
	// +7.03 -> +5.54 at 6-14 kHz. Nothing has re-measured the duty cycle at 0.45.
	//
	// The lesson is about the measurement, not the number: a level set by ear against ONE
	// balance ("can I hear the ride?") has no way to notice the other one ("is the ride now the
	// loudest thing in its band?"), and a +10 dB single step is where that goes wrong. The
	// duty-cycle-in-band reading answers both and is what the next change to this should use.
	const float StrokeLevelRide = 0.45f, StrokeLevelCrash = 0.60f;

	/// <summary>The extra decay a cymbal takes on WHILE IT IS BEING PLAYED — see RenderCymbal's
	/// chokeTau. A stroke landing on a ringing cymbal excites it and damps it, because the stick is
	/// in contact with the metal, and that is the difference between a ride and a drone. It has to
	/// compound over a stroke train the way the physics does: ring time falls with frequency, so at
	/// riding eighths a train stacks the 250 Hz band +7.6 dB over a single stroke against +2.4 dB at
	/// 5 kHz, and it is the LOW ring that runs away. A flat level cut cannot fix that — it takes the
	/// attack down with the drone. An added decay is frequency-dependent in the right direction: a
	/// fixed extra rate costs a 2.5 s band most of its tail and a 0.5 s one very little.</summary>
	public const float RestrikeTau = 0.70f;

	/// <summary>How much of the measured crash ring is kept. THIS IS A MIX DECISION AND NOT A
	/// MEASUREMENT — a real crash in a room rings for three or four seconds and the analysis says
	/// so, but a crash lands at every phrase end and every section start here, so at that density
	/// the full ring never clears before the next one and the arrangement swims. The law stays
	/// legible and the departure from it is one number rather than a quietly re-fitted constant.
	/// The ride is untouched: it is struck far more often but its strokes damp each other
	/// (RestrikeTau), which is the physical version of the same problem.</summary>
	const float CrashRingScale = 0.45f;

	/// <summary>THE STROKE HAS TO CUT, and level is the wrong lever for that. Measured against the
	/// hi-hat it replaces, the ride carries MORE energy — and it still disappears in a band mix,
	/// because of where that energy sits: a hat is high-passed noise with essentially everything
	/// above 5 kHz, a band nothing else in the arrangement occupies, while the ride's measured
	/// sustain is a bump at 1.2 kHz spreading flat from 180 Hz up, straight through the guitars.
	/// Equal energy, very unequal audibility, and raising the level only adds to the part that is
	/// masked. The splash is the answer and it is faithful to the measurement: the sustain is
	/// mid-centred but the ATTACK is broadband, so a bigger splash is a per-stroke click in the
	/// clear rather than more mud. That is what a ride's ping is, and it is inside the 0.5–1.8
	/// band the audition approved.</summary>
	/// <remarks>THE KNEE IS WHY A RIDE STROKE STOPS READING AS A CRASH. Measured per band as the
	/// time to fall 20 dB, the ride against the bright crash was low 2.80 s vs 1.00, mid 2.53 vs
	/// 0.89, upper-mid 2.03 vs 0.87, top 1.53 vs 0.81 — while the two spectra's band WEIGHTS are
	/// within a dB of each other (mid −8.4 dB vs −8.5). A ride whose spectral balance is a crash's
	/// and whose tail is 2.8× longer is a crash that will not stop, and that is what it sounded
	/// like wherever the mix got sparse enough to expose it. The crash already carried a knee for
	/// this reason; the ride carried none, so its mids rang at the full τ=k/√f. At 2 kHz the mid
	/// comes to 1.47 s — still 1.65× the crash, because a ride SHOULD sustain longer than one, and
	/// no longer the same object. The stick attack is untouched (peak still inside the first 20 ms),
	/// which is the point: this shortens the wash behind the ping, not the ping.</remarks>
	public static CymbalBands Bow( float splash = 1f, float wash = 1f, float ring = 1f )
		=> Build( BowStrike, tauK: 39f, knee: 3500f, sizzle: 0f, ring: ring,
			splash: 1.25f * splash, splashTau: 0.10f, washLvl: 0.060f * wash,
			washTau: 0.70f * ring, stick: 0.55f, stickCut: 9000f, noiseHp: 900f, washLp: 6500f,
			level: StrokeLevelRide, splashHp: 3200f );

	/// <summary>The bell. A ride bell is not a church bell: no harmonic stack and no low
	/// fundamental — the measurement puts its energy in a clang cluster around 2.3 kHz over the
	/// same metal as the bow.</summary>
	public static CymbalBands Bell( float splash = 1f, float ring = 1f, float clang = 2300f )
		=> Build( BellStrike( clang ), tauK: 39f, knee: 3500f, sizzle: 0f, ring: ring,
			splash: 0.40f * splash, splashTau: 0.05f, washLvl: 0.030f,
			washTau: 0.55f * ring, stick: 0.40f, stickCut: 6500f, noiseHp: 240f, washLp: 6500f,
			level: StrokeLevelRide, splashHp: 2600f );

	/// <summary>The bright crash. THE ROAR IS THE INSTRUMENT: a third of a second in, the
	/// measurement resolves essentially no partials at all. So the splash is not an attack
	/// transient here, it is a layer with its own third of a second of decay.</summary>
	public static CymbalBands CrashBright( float splash = 1f, float ring = 1f, float wash = 1f )
		=> Build( BrightCrashStrike, tauK: 45f, knee: 1000f, sizzle: 0f, ring: ring * CrashRingScale,
			splash: 2.30f * splash, splashTau: 0.30f, washLvl: 0.55f * wash,
			washTau: 1.05f * ring, stick: 0.10f, stickCut: 6000f, noiseHp: 2400f, washLp: 6200f,
			level: StrokeLevelCrash, splashHp: 900f );

	/// <summary>The dark crash — a heavier, flatter cymbal crashed rather than ridden, and the
	/// opposite shape at both ends: resolved lows that ring, a body gone in half a second, and a
	/// top that outlives everything.</summary>
	public static CymbalBands CrashDark( float splash = 1f, float ring = 1f, float wash = 1f )
		=> Build( DarkCrashStrike, tauK: 13.5f, knee: 0f, sizzle: 1.5f, ring: ring * CrashRingScale,
			splash: 1.50f * splash, splashTau: 0.22f, washLvl: 0.35f * wash,
			washTau: 0.90f * ring, stick: 0.10f, stickCut: 4500f, noiseHp: 200f, washLp: 4200f,
			level: StrokeLevelCrash, splashHp: 200f );

	static CymbalBands Build( in Strike strike, float tauK, float knee, float sizzle, float ring,
		float splash, float splashTau, float washLvl, float washTau, float stick, float stickCut,
		float noiseHp, float washLp, float level, float splashHp )
	{
		var hz = new float[Bands]; var am = new float[Bands]; var ta = new float[Bands];
		float step = MathF.Pow( BandHi / BandLo, 1f / (Bands - 1) );
		float e = 0f, maxTau = 0f;
		for ( int b = 0; b < Bands; b++ )
		{
			float f = BandLo * MathF.Pow( step, b );
			hz[b] = f;
			am[b] = strike.Weight( f );
			ta[b] = ring * RingTau( f, tauK, knee, sizzle );
			e += am[b] * am[b];
			maxTau = MathF.Max( maxTau, ta[b] );
		}
		// Energy-normalised, so the four strikes land comparably before the stroke level is applied.
		float lvl = level / MathF.Sqrt( MathF.Max( 1e-6f, e ) );
		// Long enough for the longest component to reach about −45 dB, and no longer: every sample
		// past that is a multiply spent on silence, and this voice is rendered per HIT.
		// Long enough for the longest band to reach about −31 dB and no longer. A cymbal in a room
		// keeps going past that; a cymbal in a mix with a whole kit over it does not, and every
		// sample past the point it stops being audible is a multiply spent on silence.
		float dur = Math.Clamp( maxTau * 3.6f, 0.6f, 3.0f );
		return new CymbalBands( hz, am, ta, dur, lvl, stick, stickCut,
			splash, splashTau, washLvl, washTau, noiseHp, washLp, splashHp );
	}
}

/// <summary>This song's cymbals as NUMBERS — the per-song point each of KitNuance's cymbal bands
/// sits at. Drawn in ComposePlan so every genre pulls the same values in the same order, for the
/// same reason a kick's click corner is drawn there: a kit is a physical object and the same
/// cymbal does not make the identical sound twice.</summary>
readonly struct CymbalDraw
{
	public readonly float RideSplash, RideWash, RideRing, BellClang, BellRing;
	public readonly float BrightSplash, BrightRing, DarkSplash, DarkWash, DarkRing;

	CymbalDraw( float rideSplash, float rideWash, float rideRing, float bellClang, float bellRing,
		float brightSplash, float brightRing, float darkSplash, float darkWash, float darkRing )
	{
		RideSplash = rideSplash; RideWash = rideWash; RideRing = rideRing;
		BellClang = bellClang; BellRing = bellRing;
		BrightSplash = brightSplash; BrightRing = brightRing;
		DarkSplash = darkSplash; DarkWash = darkWash; DarkRing = darkRing;
	}

	public static readonly CymbalDraw Default = new( 1f, 1f, 1f, 2300f, 1f, 1f, 1f, 1f, 1f, 1f );

	public static CymbalDraw Draw( Rng rng ) => new(
		KitNuance.At( KitNuance.RideSplashMin, KitNuance.RideSplashMax, rng.Next() ),
		KitNuance.At( KitNuance.RideWashMin, KitNuance.RideWashMax, rng.Next() ),
		KitNuance.At( KitNuance.RideRingMin, KitNuance.RideRingMax, rng.Next() ),
		KitNuance.At( KitNuance.BellClangMin, KitNuance.BellClangMax, rng.Next() ),
		KitNuance.At( KitNuance.BellRingMin, KitNuance.BellRingMax, rng.Next() ),
		KitNuance.At( KitNuance.CrashSplashMin, KitNuance.CrashSplashMax, rng.Next() ),
		KitNuance.At( KitNuance.CrashRingMin, KitNuance.CrashRingMax, rng.Next() ),
		KitNuance.At( KitNuance.DarkSplashMin, KitNuance.DarkSplashMax, rng.Next() ),
		KitNuance.At( KitNuance.DarkWashMin, KitNuance.DarkWashMax, rng.Next() ),
		KitNuance.At( KitNuance.DarkRingMin, KitNuance.DarkRingMax, rng.Next() ) );
}

public sealed partial class MusicGen
{
	/// <summary>A deterministic per-hit stick/round-robin stream. A LOCAL LFSR seeded on the hit's
	/// own sample position: two hits differ, the same hit is always the same, and the shared drum
	/// RNG stream — and therefore every other pattern in the song — is left byte-identical.</summary>
	static uint HitSeed( int start ) => (uint)start * 2654435761u | 1u;

	static float HitNext( ref uint s )
	{
		s ^= s << 13; s ^= s >> 17; s ^= s << 5;
		return (s & 0xffff) / 32768f - 1f;      // −1 .. 1
	}

	// ── Kick ──
	// The kit's three struck voices take a LEVEL, defaulting to 1 so the groove path reads
	// exactly as it did. It exists for the fill, which is a phrase and needs dynamics: an even
	// stream of equally-loud hits reads as a wall however few of them there are.
	void RenderKick( int start, Rng noise, float amp = 1f )
		=> RenderKick( start, noise, amp, KickTone.Default, 0f );

	/// <param name="pan">−1 … +1. A double pedal is two beaters on two sides of one drum, so the
	/// alternation is a POSITION, not a second kick sound. 0 is the single pedal.</param>
	internal void RenderKick( int start, Rng noise, float amp, in KickTone k, float pan )
	{
		if ( _drumGain <= 0f ) return;
		start = Math.Max( 0, start + _time.DrumPush );

		// Round-robin: no two strokes of a real pedal are the same, and a straight sixteenth run
		// is where identical ones stop reading as a drum. Pitch and level move together, the way
		// a harder stroke does.
		float jp = 1f, jl = 1f;
		if ( k.Jitter > 0f )
		{
			uint js = HitSeed( start );
			jp = 1f + k.Jitter * 0.09f * HitNext( ref js );
			jl = 1f + k.Jitter * 0.16f * HitNext( ref js );
		}

		float gL = 1f, gR = 1f;
		if ( pan != 0f ) StereoGains( pan, out gL, out gR );

		int dur = (int)(_sr * k.Dur);
		double decay = dur * k.DecayFrac;
		double subDecay = dur * k.SubDecayFrac;
		double phase = 0, subPhase = 0;
		// noise.Next() only fires inside the click below, so changing dur/decay here does NOT
		// shift the drum RNG stream (patterns are preserved).
		int clickLen = (int)(_sr * k.ClickSec);
		float ca = k.ClickCut > 0f ? LpCoeff( k.ClickCut ) : 0f;
		float clickLp = 0f;
		int end = Math.Min( _bufL.Length, start + dur );
		for ( int i = 0; start + i < end; i++ )
		{
			float t = (float)i / dur;
			phase += (k.StartHz - k.DropHz * MathF.Min( 1f, t * k.DropRate )) * jp / _sr;
			subPhase += k.SubHz / _sr;
			float env = (float)Math.Exp( -i / decay );
			float subEnv = (float)Math.Exp( -i / subDecay );
			float body = (float)Math.Tanh( MathF.Sin( (float)(phase * 2 * Math.PI) ) * k.Drive ) * env;
			float sub = MathF.Sin( (float)(subPhase * 2 * Math.PI) ) * k.SubLevel * subEnv;
			float click = 0f;
			if ( i < clickLen )
			{
				float cn = noise.Next() * 2f - 1f;
				if ( k.ClickCut > 0f ) { clickLp += ca * (cn - clickLp); cn = clickLp; }
				click = cn * k.ClickLevel * (1f - i / (float)clickLen);
			}
			float v = (body + sub + click) * amp * jl * _c.KickVol * _c.KickBalance * _drumGain * _drumLowMul;
			_bufL[start + i] += v * gL; _bufR[start + i] += v * gR;
		}

		// The beater coming back off the head. It is the SAME stroke, not a second note, so it
		// carries no click of its own and cannot recurse.
		if ( k.Beater > 0f )
			RenderKick( start + (int)(_sr * k.BeaterSec), noise, amp * k.Beater,
				k.With( clickLevel: 0f, beater: 0f, jitter: 0f ), pan );
	}

	// One-pole high-pass coefficient (unconditionally stable).
	float HpCoeff( float fc ) => (float)(1.0 / (1.0 + 2 * Math.PI * fc / _sr));

	// One-pole low-pass coefficient.
	float LpCoeff( float fc ) => (float)(1.0 - Math.Exp( -2 * Math.PI * fc / _sr ));

	// ── Snare ──
	void RenderSnare( int start, Rng noise, bool ghost, float level = 1f )
		=> RenderSnare( start, noise, level, ghost ? SnareTone.Ghost : SnareTone.Default );

	internal void RenderSnare( int start, Rng noise, float level, in SnareTone s )
	{
		if ( _drumGain <= 0f ) return;
		start = Math.Max( 0, start + _time.DrumPush );
		// dur and the single noise.Next()/sample are kept exactly so the drum RNG stream
		// is unchanged — only the timbre is a parameter.
		int dur = (int)(_sr * s.Dur);
		double decay = dur * s.DecayFrac;
		double phase = 0, phase2 = 0;
		float amp2 = level * _c.SnareVol * _c.SnareBalance * s.Level * _drumGain;
		float a = HpCoeff( s.WireCut );
		var crack = s.CrackLevel > 0f ? new BandPass( s.CrackHz, s.CrackQ, _sr ) : default;
		double crackDecay = dur * s.CrackDecayFrac;
		double thudPhase = 0;
		float inPrev = 0f, outPrev = 0f;
		int end = Math.Min( _bufL.Length, start + dur );
		for ( int i = 0; start + i < end; i++ )
		{
			float t = (float)i / dur;
			float env = (float)Math.Exp( -i / decay );
			float drop = 1f - s.Sag * t;       // shell pitch sags a touch → "dow"
			phase += s.Hz1 * drop / _sr;
			phase2 += s.Hz2 * drop / _sr;
			float n = noise.Next() * 2f - 1f;
			float hp = a * (outPrev + n - inPrev); inPrev = n; outPrev = hp;
			float body = (MathF.Sin( (float)(phase * 2 * Math.PI) )
				+ MathF.Sin( (float)(phase2 * 2 * Math.PI) ) * s.Body2) * s.BodyLevel;
			float v = ((float)Math.Tanh( hp * s.WireDrive ) * s.Wire + body) * env;
			if ( s.CrackLevel > 0f )
				v += crack.Next( n ) * s.CrackLevel * (float)Math.Exp( -i / crackDecay );
			if ( s.ThudLevel > 0f )
			{
				thudPhase += s.ThudHz / _sr;
				v += MathF.Sin( (float)(thudPhase * 2 * Math.PI) ) * s.ThudLevel
					* (float)Math.Exp( -i / (dur * 0.09) );
			}
			v = v * amp2;
			_bufL[start + i] += v; _bufR[start + i] += v;
		}
	}

	internal void RenderSnare( int start, Rng noise, SnareHit hit, float amp = 1f )
		=> RenderSnare( start, noise, amp, SnareTone.For( hit ) );

	/// <summary>A flam: the grace note is a hand that arrives early and quieter, and the pair is
	/// heard as ONE thickened stroke rather than as two notes. The spacing is in MILLISECONDS —
	/// it is a physical property of two sticks, not a subdivision, so it must not scale with
	/// tempo (see the strum-spread note in CLAUDE.md).</summary>
	internal void RenderSnareFlam( int start, Rng noise, float amp = 1f, float graceMs = 24f,
		float graceLevel = 0.55f )
	{
		RenderSnare( start - (int)(_sr * graceMs * 0.001f), noise, amp * graceLevel, SnareTone.Default );
		RenderSnare( start, noise, amp, SnareTone.Default );
	}

	/// <summary>A buzz/press roll across a span: the stick is leaned into the head and the
	/// bounces run together. Many quiet, closely-spaced strokes, so it is a texture with a
	/// crescendo rather than a rhythm.</summary>
	internal void RenderSnareBuzz( int start, int spanSamples, Rng noise, float fromAmp = 0.18f,
		float toAmp = 0.85f, float spacingMs = 26f )
	{
		int step = Math.Max( 1, (int)(_sr * spacingMs * 0.001f) );
		var tone = SnareTone.Default.With( dur: 0.05f, decayFrac: 0.26, wire: 0.85f );
		for ( int i = 0, at = 0; at < spanSamples; i++, at += step )
		{
			float u = spanSamples <= step ? 1f : at / (float)spanSamples;
			uint s = HitSeed( start + at );
			float wobble = 1f + 0.18f * HitNext( ref s );
			RenderSnare( start + at, noise, (fromAmp + (toAmp - fromAmp) * u) * wobble, tone );
		}
	}

	// ── Toms ──

	/// <summary>A tom of the kit, by INDEX. 0 is the rack, 2 the floor; where it sits in the
	/// field is a property of which drum it is, so no fill can drive it off the end of a range.
	/// </summary>
	internal void RenderTom( int start, in TomKit kit, int index, Rng noise, float amp, in TomTone tone )
	{
		if ( _drumGain <= 0f ) return;
		StereoGains( -_drumPan * kit.Pan( index ), out float gL, out float gR );
		RenderTomAt( start, kit.Hz( index ), gL, gR, amp, tone );
	}

	void RenderTomAt( int start, float baseFreq, float gL, float gR, float amp, in TomTone k )
	{
		start = Math.Max( 0, start + _time.DrumPush );
		int dur = (int)(_sr * k.Dur);
		double decay = dur * k.DecayFrac;
		double attackDecay = dur * k.SnapDecayFrac;   // fast-decaying upper partial → beater "snap"
		double phase = 0, phase2 = 0;
		uint ns = HitSeed( start );
		int clickLen = (int)(_sr * k.ClickSec);
		int end = Math.Min( _bufL.Length, start + dur );
		for ( int i = 0; start + i < end; i++ )
		{
			float t = (float)i / dur;
			float pf = baseFreq * (1f - k.Sag * t);    // pitch sag: how far the head lets go
			phase += pf / _sr;
			phase2 += pf * k.SnapMul / _sr;            // inharmonic upper partial for attack snap
			float env = (float)Math.Exp( -i / decay );
			float aenv = (float)Math.Exp( -i / attackDecay );
			float body = MathF.Sin( (float)(phase * 2 * Math.PI) ) * env;
			float snap = MathF.Sin( (float)(phase2 * 2 * Math.PI) ) * aenv * k.SnapLevel;
			float click = 0f;
			if ( i < clickLen )
				click = HitNext( ref ns ) * k.ClickLevel * (1f - i / (float)clickLen);
			float v = (body + snap + click) * amp * _c.TomVol * _c.TomBalance * _drumGain * _drumLowMul;
			_bufL[start + i] += v * gL; _bufR[start + i] += v * gR;
		}
	}

	// ── Hats ──

	/// <summary>The hi-hat. OPENNESS IS A CONTINUUM — a pedal is a distance, not a switch — and
	/// <paramref name="chokeAt"/> is where the foot closes it again. An open hat with nothing
	/// choking it rings through whatever comes next, which is why pop's open-on-every-offbeat
	/// smears: the tail is longer than the gap. The open→closed pair is the gesture.</summary>
	/// <param name="chokeAt">Absolute sample position the foot closes at, or int.MaxValue.</param>
	internal void RenderHat( int start, float openness, float amp, Rng noise, in HatTone h,
		int chokeAt = int.MaxValue )
	{
		if ( _drumGain <= 0f ) return;
		start = Math.Max( 0, start + _time.DrumPush );
		// Closed/open hats live on the left of the kit (the hi-hat stand); ride sits opposite.
		StereoGains( -_drumPan, out float gL, out float gR );
		openness = Math.Clamp( openness, 0f, 1f );
		// THE MAP IS GEOMETRIC. A closed hat and an open one are a factor of seventeen apart in
		// length, and the ear reads that as a RATIO rather than as a difference: interpolated
		// linearly, most of the pedal's travel lands within a few percent of fully open, and two
		// positions a third of the range apart are the same hat. Stepping by a constant ratio puts
		// an even amount of audible change under every part of the travel, which is what a foot
		// lifting steadily has to sound like.
		float u = h.OpenCurve == 1f || openness <= 0f || openness >= 1f
			? openness : MathF.Pow( openness, h.OpenCurve );
		float durSec = openness <= 0f ? h.ClosedDur
			: openness >= 1f ? h.OpenDur
			: h.ClosedDur * MathF.Pow( h.OpenDur / h.ClosedDur, u );
		float cut = openness <= 0f ? h.ClosedCut
			: openness >= 1f ? h.OpenCut
			: h.ClosedCut * MathF.Pow( h.OpenCut / h.ClosedCut, u );
		int dur = (int)(_sr * durSec);
		double decay = dur * h.DecayFrac;
		float a = HpCoeff( cut );
		// The choke itself: the cymbals meeting is a fast release, not a cut — a hard stop
		// clicks, and a drummer's foot is not instantaneous either.
		float chokeStep = (float)Math.Exp( -1.0 / Math.Max( 1.0, _sr * 0.012 ) );
		float chokeEnv = 1f;
		double lowPhase = 0, sizzlePhase = 0;
		int attack = (int)(_sr * h.AttackSec);
		// Loose cymbals rattle most when they are half touching, and not at all when open or shut.
		float sizzle = h.SizzleDepth * 4f * u * (1f - u);
		float inPrev = 0f, outPrev = 0f;
		int end = Math.Min( _bufL.Length, start + dur );
		for ( int i = 0; start + i < end; i++ )
		{
			float env = (float)Math.Exp( -i / decay );
			if ( attack > 0 && i < attack ) env *= i / (float)attack;
			if ( sizzle > 0f )
			{
				sizzlePhase += h.SizzleHz / _sr;
				env *= 1f - sizzle * 0.5f * (1f + MathF.Sin( (float)(sizzlePhase * 2 * Math.PI) ));
			}
			float n = noise.Next() * 2f - 1f;
			float hp = a * (outPrev + n - inPrev); inPrev = n; outPrev = hp;
			float v = hp * env;
			if ( h.LowThud > 0f )
			{
				lowPhase += 96f / _sr;
				v += MathF.Sin( (float)(lowPhase * 2 * Math.PI) ) * h.LowThud
					* (float)Math.Exp( -i / (dur * 0.12) );
			}
			if ( start + i >= chokeAt ) chokeEnv *= chokeStep;
			// Left to right, and the two new factors last: at their neutral 1f they are exact
			// identities, so the groove path's arithmetic is unchanged to the bit.
			v = v * amp * _c.HatBalance * _drumGain * _drumHighMul * chokeEnv * h.Level;
			_bufL[start + i] += v * gL; _bufR[start + i] += v * gR;
		}
	}

	internal void RenderHat( int start, HatHit hit, float amp, Rng noise )
		=> RenderHat( start, hit == HatHit.Splash ? 1f : 0f, amp, noise, HatTone.For( hit ) );

	// ── Cymbals ──
	// The ride, its bell, and the two crashes: one voice, four sets of constants (CymbalBands).
	// Rendered PER HIT, like every other voice in this kit — seven filtered-noise bands and two
	// partials is cheap enough that a riding section can afford it, which is the whole reason the
	// distilled version exists.

	/// <summary>A hand on the metal: the cymbal stops. Fast, but not a cut — a hard stop clicks,
	/// and a hand is not instantaneous either.</summary>
	internal const float HandChoke = 0.020f;

	/// <param name="chokeAt">Absolute sample position something lands on the cymbal, or
	/// int.MaxValue.</param>
	/// <param name="chokeTau">How fast it takes the ring away. <see cref="HandChoke"/> is a hand
	/// and the cymbal is gone; <see cref="CymbalBands.RestrikeTau"/> is the STICK LANDING AGAIN,
	/// which is the same event seen from the other end and is not a smaller choke — it is a
	/// shorter decay for as long as the cymbal is being played, so it compounds over a stroke
	/// train the way the physics does.</param>
	/// <summary>The ride and its bell, on the right of the kit opposite the hats.</summary>
	void RenderRideCym( int at, float amp, float[][] t, int chokeAt = int.MaxValue,
		float chokeTau = HandChoke )
		=> RenderCymbal( at, amp, t, _c.RideBalance, _drumPan, chokeAt, chokeTau );

	/// <summary>A crash. The kit's two crashes are panned apart and which side each is on is the
	/// song's own draw, so a ridden crash and the one accenting over it land opposite each other
	/// for free.</summary>
	void RenderCrashCym( int at, float amp, float[][] t, bool dark,
		int chokeAt = int.MaxValue, float chokeTau = HandChoke )
		=> RenderCymbal( at, amp, t, _c.CrashBalance,
			dark == _crashBrightLeft ? _drumPan : -_drumPan, chokeAt, chokeTau );

	/// <summary>
	/// SYNTHESISED ONCE PER SONG, THEN STAMPED. The distillation fixed what the cymbal IS; it does
	/// not fix what a ride COSTS, because that is a property of the pattern: a 2.5-second ring
	/// struck eight times a bar overlaps itself twenty deep, and rendering each stroke in full pays
	/// for all of it. Synthesising the object once and adding it per hit is what a sampler does,
	/// and it is honest here for the same reason the bands are — a cymbal is one physical object
	/// and every strike is that object.
	///
	/// It is two tables, split at 2.5 kHz, because a soft stroke is DARKER and not merely quieter.
	/// Variants are round robins: they cost about three milliseconds each now, where the mode
	/// forest's cost a quarter of a second, so the repeat-tell is cheap to break.
	/// </summary>
	internal float[][] BuildCymbal( in CymbalBands c, int variant )
	{
		int dur = (int)(_sr * c.Dur);
		var lo = new float[dur]; var hi = new float[dur];
		SynthCymbal( c, lo, hi, (uint)(variant * 2654435761u) | 1u );
		return new[] { lo, hi };
	}

	/// <param name="chokeAt">Absolute sample position something lands on the cymbal.</param>
	/// <param name="chokeTau">How fast it takes the ring away — <see cref="HandChoke"/> for a hand,
	/// <see cref="CymbalBands.RestrikeTau"/> for the stick landing again.</param>
	internal void RenderCymbal( int start, float amp, float[][] t, float balance, float pan,
		int chokeAt = int.MaxValue, float chokeTau = HandChoke )
	{
		if ( _drumGain <= 0f || amp <= 0f || t == null ) return;
		start = Math.Max( 0, start + _time.DrumPush );
		int end = Math.Min( _bufL.Length, start + t[0].Length );
		if ( end <= start ) return;
		StereoGains( pan, out float gL, out float gR );
		uint js = HitSeed( start );
		float jit = 1f + 0.07f * HitNext( ref js );
		float bus = balance * _drumGain * _drumHighMul * jit;
		float loG = amp * bus;
		// A soft stroke is darker, not merely quieter — a stick that does not dig in leaves the top
		// of the cymbal alone.
		float hiG = amp * MathF.Pow( Math.Clamp( amp, 0.05f, 1f ), 0.35f ) * bus;
		float chokeStep = (float)Math.Exp( -1.0 / Math.Max( 1.0, _sr * (double)chokeTau ) );
		float chokeEnv = 1f;
		for ( int i = 0; start + i < end; i++ )
		{
			if ( start + i >= chokeAt )
			{
				chokeEnv *= chokeStep;
				if ( chokeEnv < 1e-4f ) break;
			}
			float v = (t[0][i] * loG + t[1][i] * hiG) * chokeEnv;
			_bufL[start + i] += v * gL; _bufR[start + i] += v * gR;
		}
	}

	/// <summary>The voice itself: seven filtered-noise bands with their own decays, the low pair,
	/// splash and wash. Written into a lo/hi pair so a stroke's brightness can vary at stamp time.
	/// </summary>
	void SynthCymbal( in CymbalBands c, float[] outLo, float[] outHi, uint seed )
	{
		int dur = outLo.Length;

		int nb = c.Hz.Length;
		var bp = new BandPass[nb];
		var env = new float[nb]; var dec = new float[nb];
		var dies = new int[nb];
		for ( int b = 0; b < nb; b++ )
		{
			bp[b] = new BandPass( c.Hz[b], CymbalBandQ, _sr );
			env[b] = 1f;
			dec[b] = (float)Math.Exp( -1.0 / (_sr * (double)c.Tau[b]) );
			// Each band stops when it stops being worth its multiplies; they differ by a factor of
			// five in ring time, so most are gone long before the table is.
			dies[b] = Math.Min( dur, (int)(_sr * c.Tau[b] * 5.0f) + 1 );
		}

		uint ns = seed;

		int stickLen = (int)(_sr * 0.004f);
		float sa = c.StickCut > 0f ? LpCoeff( c.StickCut ) : 0f;
		float lp = 0f;
		float hpA = HpCoeff( c.NoiseHp ), washLpA = LpCoeff( c.WashLp );
		float splHpA = HpCoeff( c.SplashHp );
		float nInPrev = 0f, nHpPrev = 0f, washLp = 0f, sInPrev = 0f, sHpPrev = 0f;
		double splDecay = _sr * (double)Math.Max( 0.005f, c.SplashTau );
		double washDecay = _sr * (double)Math.Max( 0.02f, c.WashTau );
		// The fade keeps the truncation silent: the longest band still has tail left at the end.
		int fade = (int)(_sr * 0.10f);
		for ( int i = 0; i < dur; i++ )
		{
			float n = noiseNext( ref ns );
			float lo = 0f, hi = 0f;
			for ( int b = 0; b < nb; b++ )
			{
				if ( i >= dies[b] ) continue;
				float v = bp[b].Next( n ) * c.Amp[b] * env[b];
				env[b] *= dec[b];
				if ( c.Hz[b] >= BandSplit ) hi += v; else lo += v;
			}
			// The splash (broadband, and on a crash it keeps going for a third of a second) and the
			// wash (the air, darker and long) — the layers the mode forest never carried anyway.
			float hp = hpA * (nHpPrev + n - nInPrev); nInPrev = n; nHpPrev = hp;
			washLp += washLpA * (hp - washLp);
			// The splash rides in the high table and the wash in the low, so each tilts with the
			// layer it belongs to.
			float shp = splHpA * (sHpPrev + n - sInPrev); sInPrev = n; sHpPrev = shp;
			hi += shp * c.SplashLvl * (float)Math.Exp( -i / splDecay );
			lo += washLp * c.WashLvl * (float)Math.Exp( -i / washDecay );
			if ( c.Stick > 0f && i < stickLen )
			{
				float sn = HitNext( ref ns );
				if ( c.StickCut > 0f ) { lp += sa * (sn - lp); sn = lp; }
				hi += sn * c.Stick * (1f - i / (float)stickLen);
			}
			int rem = dur - i;
			float k = c.Level * (rem < fade ? rem / (float)fade : 1f);
			outLo[i] = lo * k; outHi[i] = hi * k;
		}
	}

	/// <summary>Where the tilt splits the cymbal: above this is stick and shimmer, below it is the
	/// body.</summary>
	const float BandSplit = 2500f;

	const float CymbalBandQ = 0.7f;

	/// <summary>The cymbal's noise source. Its own per-hit stream, so no two strokes are the same
	/// waveform and the shared drum RNG is untouched — the thing a rendered-once table had to buy
	/// back with round robins, and gets here for free.</summary>
	static float noiseNext( ref uint s ) => HitNext( ref s );
}
gamah.skafinity / Code/Engine/Melody.cs
Game library
using System;
using System.Collections.Generic;

namespace Skafinity;

/// <summary>
/// The TUNE — the part of a song a listener could hum back.
///
/// Everything the engine generated before this was accompaniment plus an improvisation: the
/// chordal voices played a rhythm figure, and the lead invented a fresh phrase every two bars.
/// That is a backing track, not a song. Real rock, punk, ska and pop songs are built on a
/// MELODY that recurs — the chorus states the same tune every time it comes round, and that
/// repetition is what makes it a chorus rather than another eight bars.
///
/// A tune is a <see cref="Pattern"/> whose cell values are SCALE DEGREES relative to the key's
/// tonic (not to the current chord), so the line keeps its shape while the harmony moves under
/// it — which is what a melody is. <see cref="MusicGen.RenderTune"/> resolves a degree against
/// the bar's chord on the strong beats, so the tune stays consonant without being re-written
/// chord by chord.
///
/// Because it is a Pattern it inherits everything patterns get: it anchors to the section (so a
/// four-bar tune restarts with the chorus) and it stretches under a half-time feel.
/// </summary>
static class Melody
{
	/// <summary>Cell value for a rest — no onset, the previous note holds.</summary>
	public const int Rest = Harmony.Rest;

	/// <summary>The AMBITUS — the range a whole tune is written in, in SCALE DEGREES from the key's
	/// tonic: from the sixth below it up to the third above the octave. Twelve degrees, about an
	/// octave and a fifth in a major scale, and deliberately lopsided — a melody sits above its
	/// tonic and only dips under it, so a symmetric range would spend half of itself where no tune
	/// goes.
	///
	/// It is an authored bound rather than a measured one: what it is FOR is that a line which
	/// wanders further stops being singable, and singable is what makes the thing a tune. The
	/// number is a judgement about that and nothing more.</summary>
	public const int DegreeMin = -2, DegreeMax = 9;

	/// <summary>How many scale degrees ONE PHRASE may cover — eight, an octave in a major scale,
	/// inside the twelve the whole tune may reach.
	///
	/// A RANGE AND AN AMBITUS ARE TWO DIFFERENT NUMBERS AND ONE CANNOT DO BOTH JOBS. The ambitus is
	/// a whole-song figure — how far the tune goes over all of it — and a tune here is 2–8 bars, so
	/// bounding a single phrase with it was measuring one thing and spending it on another. What a
	/// phrase actually does is orbit a register: it opens somewhere, moves about an octave around
	/// that, and the tune gets its wider reach from the phrases sitting in DIFFERENT places rather
	/// than from any one of them wandering.
	///
	/// So the window is drawn per phrase and anchored on the note the phrase opens on
	/// (<see cref="Opens"/>), which is why the opening degree keeps its weighting instead of being
	/// folded into a window drawn first. Inside a phrase this is the bound the walk reflects off
	/// and the centre <see cref="Centre"/> pulls toward; the ambitus stays the outer wall.
	///
	/// THE WINDOW BOUNDS WHERE A LINE WANDERS, NOT WHERE IT MAY BE PUT. An answer transposes its
	/// call bodily (<see cref="AnswerOp.SequenceUp2"/> takes it up two degrees), and that is a
	/// deliberate move rather than a walk drifting out of register — so <see cref="Answer"/>
	/// reflects off the ambitus. Folding a sequence back into the call's window would flatten the
	/// one gesture in the tune whose whole point is that it goes somewhere else.
	///
	/// Authored like everything else here (there is no melodic corpus in this repo). The published
	/// pop-melody work that gives whole-song ambitus at around two octaves measures range on a
	/// rolling two-bar window for exactly this reason, but its figures are for a different roster
	/// and are not borrowed as a number — this is a judgement about a phrase being one gesture in
	/// one register, and <c>--stats</c> reports what the engine actually does with it.</summary>
	public const int PhraseSpan = 8;

	/// <summary>The note lengths a tune may be written in, in ticks: sixteenth, eighth, dotted
	/// eighth, quarter, dotted quarter, half. <see cref="Timing.TicksPerBeat"/> is 48, so every one
	/// of them is exact and <see cref="Timing"/> needs nothing — the same clean division the
	/// thirty-second work already proved.
	///
	/// The line every genre's weights are split on is the QUARTER: the first three are shorter than
	/// a beat and the last three are a beat or longer, which is what <c>move</c> leans on to make a
	/// verse sparser than its chorus without a second density mechanism.</summary>
	public static readonly int[] Lengths = { 12, 24, 36, 48, 72, 96 };

	/// <summary>Index of the first length that is a beat or longer.</summary>
	const int LongFrom = 3;

	/// <summary>How a phrase answers itself. The answer used to not be DRAWN at all: it was the
	/// call's degrees minus one, every genre, every song, with a forced tonic on the end — so half
	/// of every tune in the engine was a mechanical transform of the other half.</summary>
	public enum AnswerOp
	{
		/// <summary>The call a step lower — the old behaviour, and still the heaviest weight in
		/// most genres because it is genuinely the commonest answer in this music.</summary>
		Transpose,
		/// <summary>The same line with a different landing: identical degrees, and the last two
		/// re-drawn to step home. What a chorus does.</summary>
		NewTail,
		/// <summary>The call restated a degree higher — a question answered with a bigger question,
		/// which still resolves because the last note is the tonic either way.</summary>
		SequenceUp,
		/// <summary>Restated two degrees higher.</summary>
		SequenceUp2,
		/// <summary>Mirrored about the call's first degree: where the call rose, the answer falls.
		/// </summary>
		Invert,
	}

	public static readonly AnswerOp[] Answers =
	{
		AnswerOp.Transpose, AnswerOp.NewTail, AnswerOp.SequenceUp, AnswerOp.SequenceUp2, AnswerOp.Invert,
	};

	/// <summary>How the CONSEQUENT opens — the one decision that makes a period a period.
	///
	/// A period is two call/answer pairs: an antecedent that leaves the line open and a consequent
	/// that closes it. Both pairs are built by exactly the machinery below; what varies is how much
	/// of the antecedent's call the consequent's call keeps. That is the classical taxonomy and it
	/// is also the whole variation budget — the answers repeat their own call's rhythm either way,
	/// so if the consequent's call does not move, nothing in the tune's second half is new.</summary>
	public enum PeriodShape
	{
		/// <summary>PARALLEL — the consequent restates the call note for note and differs only in
		/// how it answers. The commonest period in this music, and the one that makes the tune
		/// unmistakably one tune; it is also the least new material, which is why it is not the
		/// only shape.</summary>
		Parallel,
		/// <summary>VARIED — the consequent keeps the call's rhythm and sings a fresh contour over
		/// it. The rhythm is what a listener remembers, so this reads as the same phrase said again
		/// differently rather than as a second idea.</summary>
		Varied,
		/// <summary>CONTRASTING — the consequent opens with a phrase of its own, rhythm and all.
		/// The departure, and the only shape that puts a second rhythm in the tune.</summary>
		Contrasting,
	}

	public static readonly PeriodShape[] Shapes =
	{
		PeriodShape.Parallel, PeriodShape.Varied, PeriodShape.Contrasting,
	};

	/// <summary>Authored, not measured — there is no melodic corpus in this repo (see the note on
	/// <c>GenreProfile.Tune</c>) and this is a judgement about how much a tune may move under
	/// itself. It is one table rather than six because nothing found says a genre has an opinion
	/// about it; a genre that turns out to want one puts weights in <see cref="TuneVocab"/>, the
	/// way <see cref="Answers"/> already does.</summary>
	static readonly int[] ShapeWeights = { 4, 3, 3 };

	/// <summary>Where an ANTECEDENT lands: a chord tone that is not the tonic, which is what leaves
	/// the line open. The fifth is the half cadence proper and takes most of the weight; the third
	/// is the softer one. Landing home here would close the tune half way through it and make the
	/// consequent an appendix rather than an answer.</summary>
	static readonly int[] HalfCadence = { 4, 2 };
	static readonly int[] HalfCadenceWeights = { 3, 2 };

	/// <summary>The fewest bars a phrase may be. A period is four phrases, so a tune shorter than
	/// four of these is two phrases and no period — a one-bar "phrase" is a fragment, and four of
	/// them is a tune that restates itself every bar, which is the defect this exists to fix
	/// arriving from the other direction.</summary>
	public const int MinPhraseBars = 2;

	/// <summary>How many phrases a tune of <paramref name="bars"/> bars is written in: four (a
	/// period) where they are long enough to be phrases, two (a plain call and answer) otherwise.
	/// </summary>
	public static int PhraseCount( int bars ) => bars >= 4 * MinPhraseBars ? 4 : 2;

	/// <summary>Where a tune may open — chord tones only, weighted toward the tonic and the fifth,
	/// with the octave reachable.</summary>
	static readonly int[] Opens = { 0, 2, 4, 7 };
	static readonly int[] OpenWeights = { 5, 3, 4, 2 };

	/// <summary>How far the phrase leans uphill at its start and downhill at its end — the MELODIC
	/// ARCH. Phrases in this music (and in every corpus anyone has counted) rise and then fall on
	/// average, and a plain random walk does not: it wanders, and the only thing that ever brought
	/// it home was the forced tonic on the last note, which is a landing with no approach to it.
	///
	/// 0 would be the old coin toss; 0.5 would make direction deterministic and turn every tune
	/// into the same hill. This is a lean on a draw, not a shape imposed on one.</summary>
	const float Arch = 0.25f;

	/// <summary>How hard the line is pulled back toward the middle of its PHRASE WINDOW —
	/// TESSITURA, the fact that a melody orbits a central pitch rather than diffusing across
	/// everything it is allowed to sing.
	///
	/// It is what actually keeps a tune off the range ends. <see cref="Reflect"/> is a BACKSTOP: it
	/// stops a line parking at a boundary, but a walk with no centre still spends its time out
	/// there, and the arch makes that worse in the first half of every phrase by leaning uphill
	/// whatever the register already is. The two are different jobs and both are needed — this
	/// decides where the line lives, reflection decides what happens when it arrives at an edge
	/// anyway. The centre it pulls toward is the PHRASE's (<see cref="PhraseSpan"/>), so a phrase
	/// orbits its own register rather than the middle of everything the tune may reach.</summary>
	const float Centre = 0.30f;

	/// <summary>
	/// Draw a tune: <paramref name="bars"/> bars built as a PERIOD where they are long enough for
	/// one, and as a plain call and answer where they are not.
	///
	/// A call and answer is a pair of phrases — the first states a shape and leaves it open, the
	/// second repeats that rhythm and resolves it home. That symmetry is most of what makes a line
	/// sound composed rather than generated, and a fresh random phrase every two bars never sounds
	/// like a tune however good the notes are.
	///
	/// A PERIOD IS TWO OF THOSE PAIRS AND SITS ABOVE THEM, NOT INSTEAD OF THEM. The antecedent
	/// (call, answer) lands on a chord tone that is not the tonic and so leaves the line open; the
	/// consequent (call, answer) opens from the antecedent — restating it, varying it, or departing
	/// from it (<see cref="PeriodShape"/>) — and resolves home. That is what puts repetition at the
	/// whole tune's length and variation at a phrase's, instead of the binary shape the tune had
	/// before this: two phrases, one rhythm between them, looped to fill the section and repeated
	/// identically at every chorus.
	///
	/// THE RHYTHM REPEAT STAYS, WITHIN A PAIR. Varying an answer's rhythm stops its two phrases
	/// being heard as a question and an answer at all; the only rhythmic freedom an answer gets is
	/// where its last notes land, and that arrives through <see cref="AnswerOp.NewTail"/> rather
	/// than through a second rhythm draw. A new rhythm enters a tune at the CONSEQUENT'S CALL or
	/// nowhere. The 100%-tonic ending stays too — that is not a defect to be varied away, it is
	/// what makes the thing a tune.
	/// </summary>
	/// <param name="v">The genre's vocabulary — the note lengths it sings in, how often it rests,
	/// how often it leaps, and how it answers itself.</param>
	/// <param name="move">How much this line moves relative to the genre's own table: 1 for a
	/// chorus, less for the sparser verse tune. It leans the length draw toward the long end rather
	/// than being a second density knob sitting beside the weights.</param>
	/// <param name="swung">True where the song swings or shuffles. THE SIXTEENTH COMES OUT OF THE
	/// MENU: under a shuffle the beat's own subdivision IS the triplet, and Timing's warp puts a
	/// straight sixteenth at a third of the beat while the band's eighth-based figures sit on the
	/// beat and at two thirds. That is not syncopation, it is two grids at once, and it reads as
	/// the lead pushing against a band it does not line up with. A shuffled genre's melody moves in
	/// eighths and the shuffle does the subdividing.</param>
	public static Pattern Draw( Rng rng, int bars, int barTicks, in TuneVocab v, float move = 1f,
		bool swung = false )
	{
		int phrases = PhraseCount( bars );
		int phraseTicks = barTicks * Math.Max( 1, bars / phrases );
		var ticks = new List<int>();
		var degrees = new List<int>();

		// The genre's length table, leaned toward the long end for a verse. At move = 1 both
		// factors are 1 and the table is the genre's verbatim.
		var weights = new int[Lengths.Length];
		for ( int i = 0; i < weights.Length; i++ )
		{
			float w = v.LengthWeights[i] * (i < LongFrom ? move : 2f - move);
			// Anything that does not divide the eighth: the sixteenth AND the dotted eighth, which
			// lands mid-eighth for the same reason and was the half of this that was easy to miss.
			if ( swung && Lengths[i] % Timing.TicksPerEighth != 0 ) w = 0f;
			weights[i] = Math.Max( 0, (int)MathF.Round( w * 8f ) );
		}

		// ── the antecedent ──
		var callRhythm = DrawRhythm( rng, phraseTicks, weights, v );
		var callDegrees = DrawContour( rng, callRhythm.Count, v );
		Emit( ticks, degrees, 0, callRhythm, callDegrees );

		// A HALF CADENCE IS WHAT MAKES THE CONSEQUENT NECESSARY. With a period the antecedent lands
		// on a chord tone that is not the tonic and stays open; with only two phrases there is
		// nothing after it, so it resolves the way it always did.
		int open = phrases == 4 ? HalfCadence[rng.WeightedIndex( HalfCadenceWeights )] : 0;
		Emit( ticks, degrees, phraseTicks, callRhythm, Answer( rng, callDegrees, v, open ) );

		if ( phrases == 4 )
		{
			var shape = rng.PickWeighted( Shapes, ShapeWeights );
			// BOTH DRAWN FOR EVERY SHAPE, so swapping one shape for another does not shift the rest
			// of the tune's stream — the discipline PickOrNull keeps in the composer. A parallel
			// consequent pays for a phrase it does not sing.
			var freshRhythm = DrawRhythm( rng, phraseTicks, weights, v );
			var conRhythm = shape == PeriodShape.Contrasting ? freshRhythm : callRhythm;
			var freshDegrees = DrawContour( rng, conRhythm.Count, v );
			var conDegrees = shape == PeriodShape.Parallel ? callDegrees : freshDegrees;

			Emit( ticks, degrees, 2 * phraseTicks, conRhythm, conDegrees );
			// The consequent answers with its own operator — that is what a parallel period varies,
			// and it is the only thing it varies.
			Emit( ticks, degrees, 3 * phraseTicks, conRhythm, Answer( rng, conDegrees, v, 0 ) );
		}

		// A held final note, so the tune breathes before it comes round again.
		return new Pattern( bars * barTicks, ticks.ToArray(), degrees.ToArray() );
	}

	/// <summary>Append one phrase's onsets (offset to <paramref name="at"/>) and its degrees.
	/// </summary>
	static void Emit( List<int> ticks, List<int> degrees, int at, List<int> rhythm, List<int> pitches )
	{
		for ( int i = 0; i < rhythm.Count; i++ ) { ticks.Add( at + rhythm[i] ); degrees.Add( pitches[i] ); }
	}

	/// <summary>One phrase's RHYTHM — the onsets, in ticks from the phrase's own start.
	///
	/// Rhythm first, and separately from the pitches: a melody's rhythm is what gets remembered,
	/// and drawing it on its own is what lets an answer repeat it exactly.
	///
	/// A REST IS AN OMITTED ONSET, not a cell. Leaving the tick out means the previous note's
	/// SpanTicks simply grows to cover the gap, and RenderTune's two-beat length cap turns the
	/// remainder into real silence — so rests cost the renderer nothing. A Melody.Rest CELL
	/// would be read as a DEGREE by RenderTune, which has no rest arm, and sung.</summary>
	static List<int> DrawRhythm( Rng rng, int phraseTicks, int[] weights, in TuneVocab v )
	{
		var rhythm = new List<int>();
		for ( int t = 0; t < phraseTicks; )
		{
			// THE PHRASE RE-ANCHORS TO THE BEAT, and without this the widened vocabulary is worse
			// than the two lengths it replaced. Free-running the accumulator over the menu means a
			// dotted eighth or a sixteenth shifts EVERY REMAINING NOTE of the phrase by a non-beat
			// amount, permanently — the line rotates against the bar and never comes back, which is
			// a 3-against-4 running for eight bars rather than a melody. It reads as the lead being
			// out of time with the band, because it is.
			//
			// The rule is the one a player reads off a stave: inside a beat you may only play what
			// fits the rest of it. So a dotted eighth is followed by a sixteenth, a sixteenth by
			// whatever fills the remaining three, and the next beat starts on the beat. Notes still
			// land on the "and" and on sixteenths — what they cannot do is drift.
			int inBeat = t % Timing.TicksPerBeat;
			int len;
			if ( inBeat == 0 ) len = Lengths[rng.WeightedIndex( weights )];
			else
			{
				int room = Timing.TicksPerBeat - inBeat;
				var fits = new int[Lengths.Length];
				bool any = false;
				for ( int i = 0; i < Lengths.Length; i++ )
					if ( Lengths[i] <= room ) { fits[i] = weights[i]; any |= weights[i] > 0; }
				len = any ? Lengths[rng.WeightedIndex( fits )] : room;
			}
			// Never open the phrase on silence: a tune that starts by not being there has no shape
			// for the answer to repeat.
			if ( rhythm.Count == 0 || !rng.Chance( v.Rest ) ) rhythm.Add( t );
			t += len;
		}
		// A call of one note is not a call. Only reachable when every cell after the first drew a
		// rest, which is rare and still worth not shipping.
		if ( rhythm.Count < 2 ) rhythm.Add( phraseTicks / 2 );
		return rhythm;
	}

	/// <summary>One phrase's CONTOUR — <paramref name="notes"/> degrees relative to the key's tonic.
	///
	/// Three things shape it and none of them is a random walk: the arch (see <see cref="Arch"/>),
	/// post-skip reversal (below), and reflection off the range ends instead of a clamp.
	///
	/// A phrase opens on a CHORD TONE — a melody that opens on the second or the seventh is a
	/// melody that starts by needing to resolve — weighted toward the tonic and the fifth, where
	/// far more tunes actually start, with the octave reachable. A uniform draw over three values
	/// is the sort of thing that shows up in a sweep as 33/33/33 and in a listen as "they all start
	/// the same way".</summary>
	static List<int> DrawContour( Rng rng, int notes, in TuneVocab v )
	{
		var degrees = new List<int>( notes );
		int degree = Opens[rng.WeightedIndex( OpenWeights )];
		// THE PHRASE'S OWN WINDOW, drawn around the note the phrase opens on so that the opening
		// degree keeps its weighting and cannot land outside its own register. Where the window may
		// sit is what gives the tune its wider reach: two phrases an octave apart cover the ambitus
		// between them without either of them wandering.
		int loMin = Math.Max( DegreeMin, degree - (PhraseSpan - 1) );
		int loMax = Math.Min( degree, DegreeMax - (PhraseSpan - 1) );
		if ( loMax < loMin ) loMax = loMin;
		int lo = Math.Min( loMin + rng.Int( loMax - loMin + 1 ), DegreeMax );
		int hi = Math.Min( lo + PhraseSpan - 1, DegreeMax );
		int owed = 0;
		for ( int i = 0; i < notes; i++ )
		{
			degrees.Add( degree );

			bool leap = rng.Next() < v.Leap;
			// A leap is a third, a fourth or a fifth. It used to be a third and nothing else, in
			// every genre and every song — "a leap" was one interval wearing a general name.
			int size = leap ? 2 + rng.Int( 3 ) : 1;
			int sign;
			if ( owed != 0 )
			{
				// POST-SKIP REVERSAL: a melody that jumps comes back. It is one of the most robust
				// findings there is about how tunes are actually written, and it is also what makes
				// a leap read as a gesture rather than as the line relocating.
				sign = owed;
				owed = 0;
			}
			else
			{
				float u = notes < 2 ? 0.5f : i / (float)(notes - 1);
				// Where in the PHRASE'S window this note sits, −1 at the bottom and +1 at the top.
				float mid = (lo + hi) / 2f, half = Math.Max( 1f, (hi - lo) / 2f );
				float pos = (degree - mid) / half;
				sign = rng.Chance( Math.Clamp( 0.5f + Arch * (1f - 2f * u) - Centre * pos, 0.05f, 0.95f ) )
					? 1 : -1;
			}
			if ( leap ) owed = -sign;
			degree = Reflect( degree + sign * size, lo, hi );
		}
		return degrees;
	}

	/// <summary>ANSWER a call: the same rhythm, the call's degrees put through one of the genre's
	/// <see cref="AnswerOp"/>s, landing on <paramref name="last"/> — the tonic where this phrase
	/// closes the tune, an open chord tone where it is an antecedent handing over to a consequent.
	/// Every operator answers the same question and every one of them lands in the same place.
	/// </summary>
	static List<int> Answer( Rng rng, List<int> call, in TuneVocab v, int last )
	{
		var op = rng.PickWeighted( Answers, v.AnswerWeights );
		// Drawn for every operator, so swapping one for another does not shift the rest of the
		// tune's stream — the same discipline PickOrNull keeps in the composer.
		int approach = rng.Chance( 0.65f ) ? 1 : -1;
		int n = call.Count;
		int first = call[0];
		var answer = new List<int>( n );
		for ( int i = 0; i < n; i++ )
		{
			if ( i == n - 1 ) { answer.Add( Reflect( last ) ); continue; }
			int d = op switch
			{
				AnswerOp.NewTail => i == n - 2 ? last + approach : call[i],
				AnswerOp.SequenceUp => call[i] + 1,
				AnswerOp.SequenceUp2 => call[i] + 2,
				AnswerOp.Invert => 2 * first - call[i],
				_ => call[i] - 1,
			};
			answer.Add( Reflect( d ) );
		}
		return answer;
	}

	/// <summary>Fold a degree back inside the singable range by REFLECTING off its ends.
	///
	/// A clamp is sticky: a line that reaches a boundary and keeps stepping outward parks there,
	/// which is where 10–18% of every tune's notes sat and where most of its repeated adjacent
	/// notes came from — the two are the same defect seen from either side. Reflection keeps the
	/// range (that is what makes a tune singable) and turns the wall into a turn.
	///
	/// Off the AMBITUS — the outer wall, which is what a transposed answer folds against.</summary>
	internal static int Reflect( int degree ) => Reflect( degree, DegreeMin, DegreeMax );

	/// <summary>Fold a degree back inside an arbitrary window by reflecting off its ends — the
	/// walk inside one phrase uses its own (<see cref="PhraseSpan"/>).</summary>
	internal static int Reflect( int degree, int lo, int hi )
	{
		for ( int guard = 0; guard < 8 && (degree < lo || degree > hi); guard++ )
		{
			if ( degree < lo ) degree = 2 * lo - degree;
			if ( degree > hi ) degree = 2 * hi - degree;
		}
		return Math.Clamp( degree, lo, hi );
	}
}

// The tune, and how a bar of it is played. Part of the MusicGen engine — see MusicGen.cs.

public sealed partial class MusicGen
{
	// The song's tunes, drawn once per song off their own streams (so having them shifts nothing
	// else in the composition) and keyed by SECTION TYPE. The chorus tune is the hook: identical
	// every chorus, which is the whole reason a chorus reads as one. The verse tune is a second,
	// sparser line — same song, different words.
	Pattern _chorusTune, _verseTune;

	// How long one PHRASE of them is. A diagnostic wanting to read a tune phrase by phrase cannot
	// re-derive this without re-deciding the period, which is the re-implementation PlanTrace
	// exists to avoid — so the composer writes it down.
	int _tunePhraseTicks;

	/// <summary>One phrase of the song's tunes, in ticks (diagnostics — see <see cref="Melody"/>).
	/// </summary>
	internal int TunePhraseTicks => _tunePhraseTicks;

	/// <summary>The tune this section sings, or null where the section is not a place for one:
	/// a solo is where the genre's lead grammar improvises, an intro is a build-in, and the
	/// ending has already resolved.</summary>
	Pattern TuneFor( Section s ) => !SectionSingsTune( s ) ? null
		: s == Section.Chorus ? _chorusTune : _verseTune;

	/// <summary>Whether a section TYPE is a place for a tune at all. Static because it is a
	/// property of the form rather than of a drawn song — which is what lets a form be checked for
	/// putting its feel changes somewhere the melody can contrast with them.</summary>
	internal static bool SectionSingsTune( Section s ) =>
		s is Section.Chorus or Section.Verse or Section.PreChorus or Section.Bridge;

	/// <summary>Draw the song's tunes — one for choruses, a sparser one for verses. Every genre
	/// gets both: "riff-led" does not mean melody-free, and metal verses with no tune left four
	/// and eight bar holes where the lead simply did not play.</summary>
	void DrawTunes( int barTicks, bool swung )
	{
		// The vocabulary is the GENRE's (GenreProfile.Tune). It used to be a switch on _prof.Lead
		// right here, which is the `if ( _genre == … )` smell one level removed: two genres sharing
		// a LeadStyle got the same tune vocabulary, and where their densities matched the draws
		// agreed and the tunes came back identical.
		// The tune is a WHOLE NUMBER OF HARMONIC CYCLES — the bars it takes the progression to come
		// round (ChordBars x the progression's length), capped at eight. A four-bar tune over an
		// eight-bar cycle states itself twice, and the second statement lands over different
		// chords than it was written against: same notes, different harmony, which is exactly the
		// "the lead clashes with the backing" it sounds like. Matching the cycle means every
		// repetition sits over the changes it was drawn for.
		int cycle = Math.Clamp( _chordBars * _prog.Length, 2, 8 );
		// A PERIOD NEEDS FOUR PHRASES, AND A WHOLE NUMBER OF CYCLES IS STILL ALIGNED TO THE CHANGES.
		// The clamp above is not about length, it is about a tune's statements landing over the
		// chords they were drawn against — and a tune of exactly two cycles does, bar for bar. So a
		// genre whose cycle is short doubles the tune rather than being stuck with two phrases: punk
		// and pop (ChordBars 1 x a four-chord progression) went from a 4-bar tune whose rhythmic cell
		// was 2 bars, stated twice and looped, to an 8-bar period. Eight is the ceiling because a
		// section is eight bars — a tune longer than the section it is sung in never finishes.
		int bars = cycle;
		while ( bars * 2 <= 8 ) bars *= 2;
		_tunePhraseTicks = barTicks * bars / Melody.PhraseCount( bars );
		// THE GENRE IS IN THE TUNE'S STREAM, and it is the only stream it is in.
		//
		// Without it the genre reached Draw through nothing but `density` and `leap`, so where two
		// genres' densities were close the draws mostly agreed and the tunes came back
		// BYTE-IDENTICAL: over 500 songs, rock and country sang the same melody 53% of the time and
		// punk and pop 52%. Same n, different key, different kit, literally the same tune — which is
		// most of why the roster read as one band.
		//
		// The SONG stream (ComposePlan's `new Rng( _tag )`) still has no genre in it, and that is a
		// feature rather than an oversight: genre 0 and genre 3 at the same tag:n share the root
		// note, the pan, the ride preference and the whole kit draw, so the same song in two genres
		// is a thing the toy can do. That is worth more than the variation putting the genre there
		// would buy, and it is why the genre goes in the TUNE streams and nowhere else.
		//
		// IT IS A DIFFERENT DRAW, NOT A GUARANTEED DIFFERENT TUNE, and that distinction is the
		// point. Two genres landing on a similar melody at one seed is the toy doing what it is
		// for; what was wrong before was that they landed there RELIABLY, off a stream that could
		// not tell them apart. Nothing here should ever grow into machinery that forces two genres
		// to diverge — the collision rate is something `--stats` reports, not something the engine
		// enforces.
		_chorusTune = Melody.Draw( new Rng( $"{_tag}:tune:{_genre}:chorus" ), bars, barTicks, _prof.Tune, 1f, swung );
		// The verse tune is the same vocabulary, sung with fewer notes in it — same song,
		// different words.
		_verseTune = Melody.Draw( new Rng( $"{_tag}:tune:{_genre}:verse" ), bars, barTicks, _prof.Tune, 0.8f, swung );
	}

	/// <summary>Play one bar of the section's tune.
	///
	/// Degrees are relative to the KEY, so the tune keeps its shape as the chords move. What
	/// keeps it consonant is resolution on the strong beats only: a note landing on a beat is
	/// pulled to the nearest tone of the bar's chord, while the notes between beats are free to
	/// pass through. Snapping everything would rewrite the tune chord by chord — which is
	/// exactly the "no tune, just an improvisation over the changes" this replaces.</summary>
	void RenderTune( Pattern tune, int barTick, int barTicks, int chord, Rng rng, Rng exprRng )
	{
		int melBase = LeadBase();
		var tones = ChordDegrees( chord );
		bool guitarLead = !_hornLead;
		float amp = (guitarLead ? _c.LeadGtrVol * _c.LeadGtrBalance : _c.MelodyVol * _c.MelodyBalance)
			* _midMul;
		float drive = guitarLead ? _c.LeadGtrDrive : _c.MelodyDrive;
		var ex = guitarLead ? Expr( "LEAD GTR" ) : Expr( "LEAD" );
		int prevMidi = NoPrev;

		// A SECTION SHORTER THAN THE TUNE SINGS THE TUNE'S END, not its beginning. A four-bar
		// pre-chorus over an eight-bar tune stated the call and was cut off by the chorus before
		// the answer ever arrived — a phrase interrupted by the next phrase, which is what "two
		// ideas at once" sounds like. Pulling the anchor back lands the tune's resolution exactly
		// on the section's last bar, which is what a pre-chorus is for.
		int anchor = _sectionTicks > 0 && _sectionTicks < tune.LengthTicks
			? _sectionTick - (tune.LengthTicks - _sectionTicks)
			: _sectionTick;
		// THE TUNE IS EXEMPT FROM THE SECTION'S FEEL, and that exemption IS half/double time.
		// Part.Feel is the RHYTHM SECTION's pattern rate: when a section halves or doubles, the
		// band changes rate underneath a vocal that stays exactly where it was — that contrast is
		// the entire gesture, and it is what makes a double-time chorus lift rather than sound
		// like the tape sped up. Scaling the hook by the same multiplier deletes the gesture and
		// leaves only a faster song. So the tune slices at the nominal rate; every other voice
		// (comp, keys, bass, horns, kit) reads _feel.
		var sung = tune.Slice( barTick, barTick + barTicks, anchor );
		Trace?.Add( TraceVoice.Tune, sung );
		foreach ( var h in sung )
		{
			int degree = h.Value;
			int len = Math.Min( h.SpanTicks, Timing.TicksPerBeat * 2 );
			bool onBeat = (h.Tick - _barTick) % Timing.TicksPerBeat == 0;

			// What resolves is the note the ear has TIME to hear against the chord: anything on a
			// beat, and anything held for a beat or more. A quick note between beats is a passing
			// tone and is left alone — that is the difference between a melody and an arpeggio.
			// (Snapping only the on-beat notes left long off-beat non-chord tones ringing over the
			// backing for up to two beats, which is what a clash sounds like.)
			bool resolve = onBeat || len >= Timing.TicksPerBeat;
			if ( resolve ) degree = NearestChordTone( tones, degree );
			int midi = ScaleMidi( melBase, degree );
			// The degree snap chose WHICH chord tone; this puts the note on the pitch the chord
			// actually sounds, which is not the same thing on every degree (see NearestSoundingTone).
			if ( resolve ) midi = NearestSoundingTone( midi, chord, h.Tick );
			// Where this note sits in the TUNE, which is the phrase a bend leans into. The tune's
			// own length is the cycle, so this is the same 0..1 whatever bar the section is on.
			float pu = ((h.Tick - anchor) % tune.LengthTicks + tune.LengthTicks)
					% tune.LengthTicks / (float)tune.LengthTicks;
			var vc = Roll( ex, midi, prevMidi, exprRng, (float)_time.SpanSeconds( h.Tick, len ),
					BendBias( len, pu ) );
			prevMidi = midi;
			RenderLeadNote( _time.TickToSample( h.Tick ), _time.SpanSamples( h.Tick, len * 0.92 ),
				midi, amp * NoteGain( h.Vel ), _time.SpanSeconds( h.Tick, len ) * 0.8,
				drive, vc );

			// The genre's own hand on the same tune: country punctuates it with double-stops, metal
			// runs between its notes. The line is the same either way — this is ORNAMENT, not a
			// different melody, which is the difference between a genre playing a song and a genre
			// having its own song. Ornament also means occasional: harmonising every long note in
			// parallel thirds replaces the melody with a two-note chord (see EmitDoubleStop).
			if ( _prof.Lead == LeadStyle.DoubleStop && len >= Timing.TicksPerEighth * 2
				&& rng.Chance( DoubleStopChance ) )
				EmitDoubleStop( h.Tick, len, degree, amp * NoteGain( h.Vel ) );
			else if ( _prof.Lead == LeadStyle.Shred && len >= Timing.TicksPerBeat && rng.Chance( 0.18f ) )
				for ( int k = 1; k <= 3; k++ )
				{
					int m2 = ScaleMidi( melBase, degree + k );
					RenderLeadNote( _time.EvenSpan( h.Tick + len / 2, len / 2, (k - 1) / 3.0 ),
						_time.SpanSamples( h.Tick, len / 8.0 ), m2, amp * 0.8f * NoteGain( h.Vel ),
						_time.SpanSeconds( h.Tick, len / 8.0 ) * 0.8, drive, vc );
				}
		}
	}
}
gamah.skafinity / Code/UI/SkafinityTheme.cs
Game library
using Sandbox;

namespace Skafinity;

/// <summary>
/// Runtime palette for <see cref="SkafinityMusicPanel"/>. The whole palette derives from one
/// hue, so a consuming game retints the board by setting a single colour:
/// <code>SkafinityTheme.Accent = Color.Parse( myAccentHex );</code>
/// Leave it unset and the board is neutral gray-on-black, which is what a drop-in library
/// should look like — colour is the consumer's call, not this library's.
/// </summary>
/// <remarks>
/// s&amp;box-only: this is UI, so it lives outside <c>Code/Engine/</c> (which stays
/// framework-free for the wasm build).
///
/// SCSS variables are compile-time, so a vendored copy could only be re-themed by editing it —
/// which is exactly what the house rule against patching a vendored library forbids. The panel
/// therefore binds these as inline <c>style=</c> values, the same way rotaliate/gambit's wall
/// boards bind <c>WallTheme</c>; <c>SkafinityMusicPanel.razor.scss</c> keeps the non-colour
/// tokens (layout, fonts, radii) and every border.
/// </remarks>
public static class SkafinityTheme
{
	// Unset = neutral. A mid gray rather than a dark one: the palette below scales DOWN for the
	// fills and UP toward white for the text, so the hue it starts from has to sit between them
	// for both ends to land — a near-black accent gives invisible tick fills.
	static readonly Color NeutralAccent = Color.Parse( "#7a7a7a" ) ?? Color.White;

	/// <summary>The hue the whole palette derives from. Null (the default) = neutral gray/black.
	/// Set it once at startup, or whenever your game's own theme changes — the panel folds this
	/// into its build hash, so a change re-renders the board.</summary>
	public static Color? Accent { get; set; }

	static Color Hue => Accent ?? NeutralAccent;

	// Derived palette. The factors are WallTheme's, so a game that passes its wall accent in gets
	// the same board it already has elsewhere — and #2f9450 reproduces the panel's original
	// hardcoded green.
	/// <summary>Board background (near-black tint of the accent).</summary>
	public static string Bg => Rgb( Scale( Hue, 0.09f ) );
	/// <summary>Button / cell / queue-entry fill — deeper than the board.</summary>
	public static string Cell => Rgb( Scale( Hue, 0.04f ) );
	/// <summary>Filled ticks and selected volume cells.</summary>
	public static string CellFill => Rgb( Scale( Hue, 0.6f ) );
	/// <summary>A wash of <see cref="CellFill"/> — a cached queue entry.</summary>
	public static string CellFillSoft => Rgba( Scale( Hue, 0.6f ), 0.25f );
	/// <summary>The accent itself: progress bars, "on" text, status lines.</summary>
	public static string AccentCss => Rgb( Hue );
	/// <summary>Background of an active toggle / the current queue entry.</summary>
	public static string AccentBg => Rgba( Hue, 0.2f );
	/// <summary>Primary text (button labels).</summary>
	public static string Text => Rgba( Mix( Hue, 0.8f ), 0.9f );
	/// <summary>Labels and secondary lines.</summary>
	public static string TextDim => Rgba( Mix( Hue, 0.72f ), 0.7f );

	// ── The same palette, as colours ──
	// Everything above is a CSS string because the razor spends it in a style= attribute. Panels this
	// library builds and styles from C# (SkafinitySlider) need the value itself, so the two that one
	// needs are exposed here as well. Same source, so they cannot drift.
	/// <summary>The accent itself. What a slider's fill and thumb are painted with.</summary>
	public static Color AccentColor => Hue;
	/// <summary>A slider's trough — neutral, like the stylesheet's borders, so it reads against any
	/// accent a host passes in.</summary>
	public static Color TrackColor => new Color( 1f, 1f, 1f, 0.18f );

	// Component-wise helpers (avoid relying on Color operators), matching the sibling repos' style.
	static Color Scale( Color c, float f ) => new Color( c.r * f, c.g * f, c.b * f );
	static Color Mix( Color c, float towardWhite ) => new Color(
		c.r + ( 1f - c.r ) * towardWhite,
		c.g + ( 1f - c.g ) * towardWhite,
		c.b + ( 1f - c.b ) * towardWhite );

	static string Rgb( Color c ) => $"rgb({To255( c.r )},{To255( c.g )},{To255( c.b )})";
	static string Rgba( Color c, float a ) => $"rgba({To255( c.r )},{To255( c.g )},{To255( c.b )},{a})";
	static int To255( float v ) => (int)System.MathF.Round( System.Math.Clamp( v, 0f, 1f ) * 255f );
}
gamah.skafinity / Engine/Arrange.cs
Game library
using System;
using System.Collections.Generic;

namespace Skafinity;

/// <summary>Which cells of a bar a voice is ALLOWED to play on. This is what keeps ska's skank
/// offbeat by RULE rather than by table — the arranger may move an onset, but not off the class
/// the genre's technique lives in, so a skank cannot drift onto the downbeat however loud the
/// accent grid is there.
///
/// It is the single property that has to survive the whole arranger, and it is the reason the
/// arranger cannot simply write onsets wherever the accent grid is loud. A genre's identity is
/// WHERE it plays; its arrangement is which of those places it uses this time.</summary>
enum CellClass
{
	/// <summary>Every sixteenth — metal's gallop, and anything that subdivides freely.</summary>
	Sixteenths,
	/// <summary>Every eighth — the rock riff, the pop pad.</summary>
	Eighths,
	/// <summary>The beats only — punk's downstrokes.</summary>
	Downbeats,
	/// <summary>The "and" of each beat only — the ska skank and country's chick.</summary>
	Offbeats,
}

/// <summary>
/// THE SECTION'S RHYTHMIC SKELETON — what every part is written against.
///
/// Before this, every voice picked its figure from its own small authored table and no voice knew
/// what any other was playing. The single exception was the riff-doubling bass, and it was the
/// only lockup in the engine that was not a coincidence: pop's pad landed on the kick 100% of the
/// time and ska's skank 1%, and neither number was decided by anyone.
///
/// The skeleton is deliberately DERIVED, not drawn. The kit is table-driven and its grooves are
/// fitted to a played corpus (see <see cref="DrumGroove"/>), so the drums are the measured
/// reference the arranger writes against rather than another client of it — which is why the
/// accent grid comes off the kick and the snare and the genre's own measured accent weights, and
/// why nothing here rolls a die to decide where the section leans.
///
/// Everything is on the section's own SIXTEENTH grid. Occupancy fills in as each part is placed,
/// so a voice arranged later can see what the ones before it took — that is what "one authority
/// arranges every part at once" amounts to in practice.
/// </summary>
sealed class Skeleton
{
	public const int CellTicks = Timing.TicksPerEighth / 2;

	/// <summary>First tick of the section, and how many sixteenth cells long it is.</summary>
	public readonly int StartTick, Cells;

	/// <summary>Cells per bar — the seam and allowed-class tests are per bar.</summary>
	public readonly int BarCells;

	/// <summary>Where the section leans, 0..1, off the groove's kick and snare and the genre's
	/// measured accent weights.</summary>
	public readonly float[] Accent;

	/// <summary>Where the kick lands. The bass's lock reads this directly rather than the accent
	/// grid — "agrees with the kick" is a different claim from "is loud in the same place".</summary>
	public readonly bool[] Kick;

	/// <summary>Phrase ends: every four bars, and the section's last bar. Where a band converges.
	/// </summary>
	public readonly bool[] Seam;

	/// <summary>Where the tune has an onset, and where it is holding a note through.</summary>
	public readonly bool[] TuneOn, TuneHold;

	/// <summary>What the parts placed so far have taken. Mutated as the arranger works down the
	/// voices, which is the whole point of arranging them in one pass.</summary>
	public readonly bool[] Taken;

	public Skeleton( int startTick, int ticks, int barTicks )
	{
		StartTick = startTick;
		Cells = Math.Max( 1, ticks / CellTicks );
		BarCells = Math.Max( 1, barTicks / CellTicks );
		Accent = new float[Cells];
		Kick = new bool[Cells];
		Seam = new bool[Cells];
		TuneOn = new bool[Cells];
		TuneHold = new bool[Cells];
		Taken = new bool[Cells];
	}

	/// <summary>The cell a SONG tick falls in, or −1 outside the section.</summary>
	public int CellAt( int tick )
	{
		int c = (tick - StartTick) / CellTicks;
		return c < 0 || c >= Cells ? -1 : c;
	}

	/// <summary>Whether a cell is one this voice's technique may play on.</summary>
	public bool Allows( CellClass cls, int cell )
	{
		int inBar = cell % BarCells;
		return cls switch
		{
			CellClass.Sixteenths => true,
			CellClass.Eighths => inBar % 2 == 0,
			CellClass.Downbeats => inBar % 4 == 0,
			_ => inBar % 4 == 2,
		};
	}
}

/// <summary>How a voice arranges itself against the skeleton: where it may play, and what it is
/// pulled toward or pushed away from. The parameters are the genre's (see
/// <see cref="GenreProfile"/>) because they say HOW a part behaves, never WHAT it plays — the
/// figure is still the genre's own authored gesture.</summary>
readonly struct ArrangeRole
{
	public readonly CellClass Cells;
	/// <summary>Pull toward cells the kick plays — how hard this voice locks to the drums.</summary>
	public readonly float Kick;
	/// <summary>Push away from cells another part has already taken, and from the tune's landings.
	/// </summary>
	public readonly float Complement;
	/// <summary>Pull toward phrase seams, where a band converges.</summary>
	public readonly float Seam;

	/// <summary>What an ADDED onset plays, where the voice's vocabulary says an addition is one
	/// particular thing rather than "another of whatever was before it". The snare is the case: a
	/// drummer filling in around a backbeat adds GHOSTS, and copying the previous cell would let a
	/// bar acquire a second struck backbeat — which is the one thing about a snare part a listener
	/// would notice immediately. <see cref="NoValue"/> keeps the copy-the-previous default.
	/// </summary>
	public readonly int AddValue;

	public const int NoValue = int.MinValue;

	public ArrangeRole( CellClass cells, float kick, float complement, float seam,
		int addValue = NoValue )
	{ Cells = cells; Kick = kick; Complement = complement; Seam = seam; AddValue = addValue; }
}

// The arranger. Part of the MusicGen engine — see MusicGen.cs.

public sealed partial class MusicGen
{
	/// <summary>The current section's skeleton, published in <c>RenderSection</c> alongside
	/// <c>_energy</c> / <c>_feel</c> / <c>_keyShift</c> — the same mechanism, so a voice reads it
	/// the way it reads those.</summary>
	Skeleton _skeleton;

	/// <summary>Build the section's skeleton, then arrange each part against it in turn.
	///
	/// ORDER IS THE DESIGN: the bass goes first because its role is to agree with the kick, which
	/// is already decided; the comp then sees the bass and the tune and can complement them; the
	/// keys see all three. Arranging them independently against a fixed grid would give every voice
	/// the same answer, which is the failure mode this whole phase has to avoid.</summary>
	void PlanArrangement( in Part part, int sectionTick, int barTicks, Pattern tune, string bk )
	{
		var sk = new Skeleton( sectionTick, _sectionTicks, barTicks );
		_skeleton = sk;
		_kitArranged = false;

		// ── the seams ──
		// A phrase ends every four bars, and the section's last bar is one whatever its length.
		for ( int bar = 4; bar * sk.BarCells < sk.Cells; bar += 4 )
			sk.Seam[bar * sk.BarCells - 1] = true;
		if ( sk.Cells > 0 ) sk.Seam[sk.Cells - 1] = true;

		// ── the tune's occupancy ──
		// The tune is written first and everything else is written against it, which is the order a
		// song is actually made in. Note that the tune is exempt from the section's feel, so it is
		// sliced at the nominal rate here exactly as RenderTune slices it.
		if ( tune != null )
		{
			int anchor = _sectionTicks > 0 && _sectionTicks < tune.LengthTicks
				? sectionTick - (tune.LengthTicks - _sectionTicks) : sectionTick;
			foreach ( var h in tune.Slice( sectionTick, sectionTick + _sectionTicks, anchor ) )
			{
				int c = sk.CellAt( h.Tick );
				if ( c < 0 ) continue;
				sk.TuneOn[c] = true;
				int held = Math.Min( h.SpanTicks, Timing.TicksPerBeat * 2 ) / Skeleton.CellTicks;
				for ( int k = 1; k < held && c + k < sk.Cells; k++ ) sk.TuneHold[c + k] = true;
			}
		}

		// ── who goes first ──
		// The skeleton the band writes against is the kit's accents, plus the genre's metric
		// weights, plus the phrase seams, plus the tune. THREE OF THOSE FOUR DO NOT NEED THE KIT,
		// which is what makes both orderings one mechanism rather than two.
		//
		// KIT LEADS: the kit arranges against seams, metre and tune; its accents then go on the
		// grid; the band follows. KIT FOLLOWS: the band writes against a grid with no kit on it and
		// the kit arranges last, against what the band actually took — a drummer playing to the
		// riff. It is not a coin flip dressed up: a leading kit is most punk and most rock, a
		// following kit is riff-led metal and a great deal of programmed pop.
		var rng = new Rng( $"{_tag}:arr:{bk}" );
		if ( _kitLeads ) { ArrangeKit( sk, bk ); KitAccents( sk ); }
		else KitAccents( sk );

		ArrangeBand( part, sk, rng );

		if ( !_kitLeads ) { ArrangeKit( sk, bk ); KitAccents( sk ); }
	}

	/// <summary>The accent grid, off the kit and the genre's own measured weights.
	///
	/// WITH NO KIT ON THE GRID YET the cells carry the metre alone — which is the honest reading of
	/// "the band writes against seams, metre and tune", and not a floor invented to keep the number
	/// non-zero: it is exactly this formula with the kit's occupancy taken as flat.</summary>
	void KitAccents( Skeleton sk )
	{
		bool haveKit = _kitArranged;
		for ( int c = 0; c < sk.Cells; c++ ) { sk.Accent[c] = 0f; sk.Kick[c] = false; }

		if ( haveKit )
		{
			foreach ( var h in _kickFig.Slice( sk.StartTick, sk.StartTick + _sectionTicks, sk.StartTick, _feel ) )
			{
				int c = sk.CellAt( h.Tick );
				if ( c < 0 ) continue;
				sk.Kick[c] = true;
				sk.Accent[c] += h.Vel;
			}
			foreach ( var h in _snareFig.Slice( sk.StartTick, sk.StartTick + _sectionTicks, sk.StartTick, _feel ) )
			{
				int c = sk.CellAt( h.Tick );
				if ( c >= 0 ) sk.Accent[c] += h.Value == DrumGroove.Ghost ? 0.3f : h.Vel;
			}
		}

		// The genre's own measured accent weights on top: a country bar leans on its offbeat and a
		// metal bar is deliberately flat, and that is a property of the genre rather than of the
		// groove it drew.
		for ( int c = 0; c < sk.Cells; c++ )
		{
			int inBar = c % sk.BarCells;
			float metric = inBar == 0 ? _prof.AccentDown
				: inBar % 4 != 0 ? _prof.AccentOff
				: (inBar / 4) % 2 == 1 ? _prof.AccentBack : 1f;
			sk.Accent[c] = Math.Min( 1f, (haveKit ? sk.Accent[c] : 1f) * metric * 0.6f );
		}
	}

	void ArrangeBand( in Part part, Skeleton sk, Rng rng )
	{
		// ── the parts ──
		// EVERY CHORUS AGREES, AND THE CHORUS IS STILL ARRANGED. Those are two different claims and
		// conflating them is what would make this whole phase a no-op: if a chorus quoted the TABLE
		// rather than quoting the other choruses, the song's own rhythm section would stay one entry
		// out of a table of three, which is the ceiling this exists to break — and the choruses are
		// most of what a listener hears as the song.
		//
		// So the chorus is arranged ONCE and cached as the song's own part. Every later chorus
		// reuses the cached line rather than re-deriving it: the guarantee becomes structural
		// instead of resting on the skeleton happening to come out the same at three different
		// points in the song.
		if ( part.Type == Section.Chorus )
		{
			if ( !_chorusArranged )
			{
				_songBass = Arrange( _songBass, sk, rng, _prof.BassRole, _prof.BassPatterns );
				_songComp = Arrange( _songComp, sk, rng, _prof.CompRole, _prof.CompFigures );
				// The LOUD figure is the chorus part in any genre that changes technique when the
				// section is loud, so leaving it un-arranged would leave exactly the bars a listener
				// remembers coming straight out of a table of two.
				if ( _songLoud != null )
					_songLoud = Arrange( _songLoud, sk, rng, _prof.LoudCompRole ?? _prof.CompRole,
						_prof.LoudCompFigures );
				if ( _songKeys != null )
					_songKeys = Arrange( _songKeys, sk, rng, _prof.KeysRole, _prof.KeysFigures );
				_chorusArranged = true;
			}
			else { MarkTaken( sk, _songBass ); MarkTaken( sk, _songComp ); MarkTaken( sk, _songKeys ); }
			_bassPat = _songBass; _compFig = _songComp; _keysFig = _songKeys;
			return;
		}

		_bassPat = Arrange( _bassPat, sk, rng, _prof.BassRole, _prof.BassPatterns );
		_compFig = Arrange( _compFig, sk, rng, _prof.CompRole, _prof.CompFigures );
		if ( _keysFig != null )
			_keysFig = Arrange( _keysFig, sk, rng, _prof.KeysRole, _prof.KeysFigures );
	}

	/// <summary>Whether the song's chorus parts have been arranged yet. The chorus is arranged the
	/// first time one is rendered and every later chorus reuses that line.</summary>
	bool _chorusArranged, _chorusKitArranged;

	/// <summary>Whether the kit's patterns for THIS section are final yet — i.e. whether the accent
	/// grid may be built off them.</summary>
	bool _kitArranged;

	/// <summary>
	/// The kit, arranged. The drums were the one layer left out of the arranger, and the reasoning
	/// was good — the grooves are fitted to a played corpus, so they were the measured reference the
	/// band wrote against rather than another client of it. The cost was that the groove was drawn
	/// ONCE PER SONG and never re-drawn: every bar of every section played the identical kick, snare
	/// and cymbal, two or three states per genre and exactly one inside a song.
	///
	/// THE CYMBAL IS NOT ARRANGED. It is the pulse, and it is where the corpus pass found the
	/// largest mismatch of all (country's hat on the "and", 84% against 36% on the beat) — leaving
	/// it alone preserves that by construction rather than by a rule that can be got wrong. What
	/// varies about the cymbal is which INSTRUMENT plays it and how sparse a section thins it, both
	/// of which already vary per section.
	///
	/// EVERY CHORUS AGREES, the same way the band's does and for the same reason: the song's kit is
	/// arranged the first time a chorus is rendered and every later chorus replays that line.
	/// </summary>
	void ArrangeKit( Skeleton sk, string bk )
	{
		var rng = new Rng( $"{_tag}:kit:{bk}" );
		if ( _sectionType == Section.Chorus )
		{
			if ( !_chorusKitArranged )
			{
				_songKick = ArrangeDrum( _songKick, sk, rng, KickRole(), kick: true );
				_songSnare = ArrangeDrum( _songSnare, sk, rng, _prof.SnareRole, kick: false );
				_chorusKitArranged = true;
			}
			_kickFig = _songKick; _snareFig = _songSnare;
			_kitArranged = true;
			return;
		}

		_kickFig = ArrangeDrum( _kickFig, sk, rng, KickRole(), kick: true );
		_snareFig = ArrangeDrum( _snareFig, sk, rng, _prof.SnareRole, kick: false );
		_kitArranged = true;
	}

	/// <summary>
	/// Where a section is quiet enough that the kit plays the SPINE and nothing else — the
	/// downbeat kick and the struck backbeat, with every ghost and every pushed kick gone.
	///
	/// AND THE FEEL GATE IS THE POINT, not a caveat. Half time is a pattern RATE: it already
	/// stretches the groove to half its density, so a breakdown that also thinned to the spine
	/// would be two hits a bar and a hole in the arrangement. Every Breakdown in every form here
	/// is <c>feel: 0.5f</c>, which is exactly why this fires on the INTRO instead — a kit walking
	/// in on the bare bones of its own groove, which is what an intro is.
	/// </summary>
	const float KitSpineFrom = 0.32f;

	/// <summary>
	/// THE KIT'S DENSITY BIAS, −1 (thin) to +1 (fill), and what makes energy an input to what the
	/// drummer PLAYS rather than only to how loud it is.
	///
	/// It shifts the weight between the DROP and ADD mutations, so a quiet section is likelier to
	/// lose an onset and a loud one to gain one. Everything a drummer does with dynamics beyond
	/// hitting harder is here: fewer notes, dropped ghosts, a busier bar under a chorus.
	///
	/// <c>DrumBusy</c> and <c>DrumTone</c> feed the same decision rather than sitting on top of it
	/// as multipliers — that is the whole reason they are here. A bright kit is snare-led, so
	/// <c>DrumTone</c> pushes the ghost layer up and the foot down; a dark one does the reverse.
	/// </summary>
	float KitBias( bool kick )
	{
		float energy = (_energy - 0.5f) * 1.4f;
		float busy = (Math.Clamp( _c.DrumBusy, 0f, 1f ) - 0.5f) * 1.2f;
		float tone = (_drumTone - 0.5f) * 0.6f * (kick ? -1f : 1f);
		return Math.Clamp( energy + busy + tone, -1f, 1f );
	}

	/// <summary>The spine alone — see <see cref="KitSpineFrom"/>.</summary>
	static Pattern ToSpine( Pattern fig, bool[] spine )
	{
		int n = 0;
		for ( int i = 0; i < spine.Length; i++ ) if ( spine[i] ) n++;
		if ( n == 0 || n == fig.Count ) return fig;
		var ticks = new int[n]; var values = new int[n]; var vels = new float[n];
		for ( int i = 0, k = 0; i < fig.Count; i++ )
		{
			if ( !spine[i] ) continue;
			ticks[k] = fig.TickAt( i ); values[k] = fig.ValueAt( i ); vels[k] = fig.VelAt( i ); k++;
		}
		return new Pattern( fig.LengthTicks, ticks, values, vels );
	}

	/// <summary>
	/// The kick's role, WITH THE SIGN OF ITS COMPLEMENT DECIDED BY WHO WROTE FIRST — and that sign
	/// is the whole difference between the two orderings.
	///
	/// <see cref="Score"/> reads <c>Complement</c> as a push AWAY from cells the tune and the parts
	/// placed so far have taken, which is right for every melodic voice and right for a kick the
	/// band has not been written against yet: a leading kit states the beat and the band answers it.
	/// A FOLLOWING kick is the opposite gesture. The riff is already on the grid, and a drummer
	/// playing to it lands WITH it — that is what "the kick tracks the topline" means in programmed
	/// pop and what a riff-led metal foot is doing under a gallop.
	///
	/// Ordering with the same sign on both sides is what the split reporting caught: it changed who
	/// saw whom and left both modes agreeing to within a point or two, which is a mechanism that
	/// costs a draw and buys nothing.
	/// </summary>
	ArrangeRole KickRole()
	{
		var r = _prof.KickRole;
		return _kitLeads ? r
			: new ArrangeRole( r.Cells, r.Kick, -r.Complement, r.Seam, r.AddValue );
	}

	/// <summary>One drum, through the same mutations as everything else — at the kit's own lower
	/// rate, with the genre's spine held back, and without writing itself into the occupancy the
	/// band reads.
	///
	/// The RECOMBINE table is this drum's line from the genre's OTHER grooves, which is the same
	/// claim the melodic version makes: the genre's own vocabulary, re-cut, and never a gesture the
	/// genre does not have.</summary>
	Pattern ArrangeDrum( Pattern fig, Skeleton sk, Rng rng, in ArrangeRole role, bool kick )
	{
		if ( fig == null ) return null;
		var spine = DrumGroove.SpineOf( fig, kick, _time.BarTicks );
		var table = new Pattern[_prof.Grooves.Length];
		for ( int i = 0; i < table.Length; i++ )
			table[i] = kick ? _prof.Grooves[i].Kick : _prof.Grooves[i].Snare;
		// The arrangement happens either way, so the stream costs the same whatever the section's
		// energy — the spine section then keeps only what it was always going to keep.
		var arranged = Arrange( fig, sk, rng, role, table, _prof.KitMutateRate, spine,
			marks: false, bias: KitBias( kick ), offPulse: kick );
		if ( _energy > KitSpineFrom || _feel < 1f ) return arranged;
		return ToSpine( arranged, DrumGroove.SpineOf( arranged, kick, _time.BarTicks ) );
	}

	/// <summary>Write a figure's onsets into the skeleton's occupancy without changing it — what a
	/// quoted part still owes the parts arranged after it.</summary>
	void MarkTaken( Skeleton sk, Pattern fig )
	{
		if ( fig == null ) return;
		foreach ( var h in fig.Slice( sk.StartTick, sk.StartTick + _sectionTicks, sk.StartTick, _feel ) )
		{
			int c = sk.CellAt( h.Tick );
			if ( c >= 0 ) sk.Taken[c] = true;
		}
	}

	/// <summary>How often a non-chorus section plays its figure verbatim rather than working on
	/// it. The rest of the weight is shared over the four mutations below.</summary>
	const float QuoteWeight = 1f;

	/// <summary>
	/// Arrange one part: the genre's authored figure, worked on against the section's skeleton.
	///
	/// THE TABLES ARE SEED MATERIAL, NOT A CEILING. The authored figures are each genre's
	/// characteristic gestures and none of them is deleted — what changes is that a section's part
	/// is now figure x mutation x skeleton rather than one entry out of a table of three. That
	/// product is where the state count comes from: the whole rhythm section used to have twelve
	/// states in punk over five hundred songs, because it was the product of three table sizes and
	/// randomness cannot reach past a table size.
	///
	/// Every mutation stays inside the genre's allowed cell class, so a skank stays offbeat and a
	/// punk downstroke stays on the beat however loud the accent grid is elsewhere.
	/// </summary>
	Pattern Arrange( Pattern fig, Skeleton sk, Rng rng, in ArrangeRole role, Pattern[] table )
		=> Arrange( fig, sk, rng, role, table, _prof.MutateRate );

	/// <param name="spine">Onsets the mutations may not reach, index-aligned with
	/// <paramref name="fig"/> — the drums' <see cref="DrumGroove.SpineOf"/>. Null for a voice whose
	/// whole figure is fair game, which is every melodic one.</param>
	/// <param name="marks">Whether the result goes into the skeleton's occupancy. The kit's does
	/// not: the kick has its own layer on the grid and the snare is most of the accent grid, so
	/// writing it into <c>Taken</c> as well would have the band pushing away from a beat it is
	/// supposed to be locking to, and count it twice while doing it.</param>
	/// <param name="bias">−1 (thin) to +1 (fill): shifts weight between DROP and ADD without
	/// changing how much of the stream the draw costs. Zero for every melodic voice, so their
	/// weights are exactly what they always were; the kit reads its section's energy and the
	/// vibe's DRUM BUSY through it (see <see cref="KitBias"/>).</param>
	Pattern Arrange( Pattern fig, Skeleton sk, Rng rng, in ArrangeRole role, Pattern[] table,
		float mutateRate, bool[] spine = null, bool marks = true, float bias = 0f,
		bool offPulse = false )
	{
		if ( fig == null || fig.Count == 0 ) { return fig; }

		// The draw is taken whatever the outcome, so a genre's mutation rate cannot change how much
		// of this stream the next voice sees — the same discipline PickOrNull keeps in the composer.
		float mutate = Math.Clamp( mutateRate, 0f, 1f );
		int op = rng.WeightedIndex( new[]
		{
			(int)MathF.Round( QuoteWeight * (1f - mutate) * 100f ),   // quote
			(int)MathF.Round( mutate * 30f * (1f - bias) ),           // drop
			(int)MathF.Round( mutate * 30f * (1f + bias) ),           // add
			(int)MathF.Round( mutate * 25f ),                         // displace
			(int)MathF.Round( mutate * 15f ),                         // recombine
		} );

		var ticks = new List<int>();
		var values = new List<int>();
		var vels = new List<float>();
		for ( int i = 0; i < fig.Count; i++ )
		{ ticks.Add( fig.TickAt( i ) ); values.Add( fig.ValueAt( i ) ); vels.Add( fig.VelAt( i ) ); }

		switch ( op )
		{
			case 1: Drop( ticks, values, vels, fig, sk, rng, role, spine ); break;
			case 2: Add( ticks, values, vels, fig, sk, rng, role, offPulse ); break;
			case 3: Displace( ticks, values, vels, fig, sk, rng, role, spine, offPulse ); break;
			case 4: Recombine( ticks, values, vels, fig, rng, table, spine ); break;
		}

		var arranged = ticks.Count == 0 ? fig
			: new Pattern( fig.LengthTicks, ticks.ToArray(), values.ToArray(), vels.ToArray() );
		if ( marks ) MarkTaken( sk, arranged );
		return arranged;
	}

	// ── scoring ──
	// A figure loops inside the section, so a candidate position is judged over EVERY repetition it
	// will actually be played at rather than over the first one. A one-bar figure in an eight-bar
	// section is played eight times; scoring it against bar 1 alone would arrange it for a bar it
	// spends seven eighths of its life away from.
	float Score( int figTick, Pattern fig, Skeleton sk, in ArrangeRole role )
	{
		float sum = 0; int n = 0;
		for ( int rep = 0; ; rep++ )
		{
			int t = sk.StartTick + (int)Math.Round( (rep * (double)fig.LengthTicks + figTick) / Math.Max( 0.01f, _feel ) );
			int c = sk.CellAt( t );
			if ( c < 0 ) break;
			sum += sk.Accent[c]
				+ role.Kick * (sk.Kick[c] ? 1f : 0f)
				+ role.Seam * (sk.Seam[c] ? 1f : 0f)
				- role.Complement * ((sk.TuneOn[c] ? 1f : 0f) + (sk.Taken[c] ? 0.6f : 0f));
			n++;
		}
		return n == 0 ? float.NegativeInfinity : sum / n;
	}

	bool AllowedFigTick( int figTick, Pattern fig, Skeleton sk, in ArrangeRole role )
	{
		int c = sk.CellAt( sk.StartTick + (int)Math.Round( figTick / Math.Max( 0.01f, _feel ) ) );
		return c >= 0 && figTick % Skeleton.CellTicks == 0 && sk.Allows( role.Cells, c );
	}

	/// <summary>DROP the onset that fights hardest with what is already there — a tune landing the
	/// comp is stepping on, most often. Never the figure's first onset: a figure that loses its
	/// downbeat is a different figure.</summary>
	void Drop( List<int> ticks, List<int> values, List<float> vels, Pattern fig, Skeleton sk,
		Rng rng, in ArrangeRole role, bool[] spine = null )
	{
		if ( ticks.Count <= 2 ) return;
		int worst = -1; float worstScore = float.MaxValue;
		for ( int i = 1; i < ticks.Count; i++ )
		{
			if ( spine != null && spine[i] ) continue;
			float s = Score( ticks[i], fig, sk, role );
			if ( s < worstScore ) { worstScore = s; worst = i; }
		}
		if ( worst < 0 ) return;
		ticks.RemoveAt( worst ); values.RemoveAt( worst ); vels.RemoveAt( worst );
	}

	/// <summary>ADD an onset on the best free cell of the genre's allowed class. The new hit takes
	/// its VALUE from the onset before it, so it is the same gesture played once more rather than a
	/// cell type the figure never used.</summary>
	/// <param name="offPulse">Refuse cells on the beat — the other half of the kick's spine law
	/// (see <see cref="DrumGroove.SpineOf"/>). A groove's identity is partly where it does NOT
	/// play, and a rule about existing onsets cannot say that: the one drop IS the hole on beat 1,
	/// and without this it quietly acquires the downbeat it is defined by not having.</param>
	void Add( List<int> ticks, List<int> values, List<float> vels, Pattern fig, Skeleton sk,
		Rng rng, in ArrangeRole role, bool offPulse = false )
	{
		int best = -1; float bestScore = float.NegativeInfinity;
		for ( int t = 0; t < fig.LengthTicks; t += Skeleton.CellTicks )
		{
			if ( offPulse && DrumGroove.IsPulse( t ) ) continue;
			if ( ticks.Contains( t ) || !AllowedFigTick( t, fig, sk, role ) ) continue;
			float s = Score( t, fig, sk, role );
			if ( s > bestScore ) { bestScore = s; best = t; }
		}
		if ( best < 0 ) return;
		int at = 0;
		while ( at < ticks.Count && ticks[at] < best ) at++;
		int from = Math.Max( 0, at - 1 );
		ticks.Insert( at, best );
		values.Insert( at, role.AddValue == ArrangeRole.NoValue ? values[from] : role.AddValue );
		vels.Insert( at, vels[from] * 0.9f );
	}

	/// <summary>DISPLACE one onset by a cell, staying inside the allowed class — the same figure
	/// with one hit pushed or pulled. Not the first onset, for the same reason DROP spares it.
	/// </summary>
	/// <param name="offPulse">As <see cref="Add"/>'s: a kick may not be moved ONTO a beat either.
	/// The same hole a groove is defined by is just as fillable by a displaced push as by an added
	/// one — ska's beat 1 still sat 3 points over its baseline once Add alone was stopped. So the
	/// kick's on-beat set is frozen entirely: nothing enters it, nothing leaves it, and what
	/// arranges is the pushes around it.</param>
	void Displace( List<int> ticks, List<int> values, List<float> vels, Pattern fig, Skeleton sk,
		Rng rng, in ArrangeRole role, bool[] spine = null, bool offPulse = false )
	{
		if ( ticks.Count <= 1 ) return;
		int i = 1 + rng.Int( ticks.Count - 1 );
		int step = rng.Chance( 0.5f ) ? Skeleton.CellTicks : -Skeleton.CellTicks;
		// Both draws are taken before the spine is consulted, so a groove whose onsets are mostly
		// spine costs this stream exactly what one whose onsets are all free costs.
		if ( spine != null && spine[i] ) return;
		// The allowed class is often coarser than a sixteenth, so widen the move rather than giving
		// up: a skank displaced by one cell can never be legal, displaced by two it is.
		for ( int k = 1; k <= 4; k++ )
		{
			int t = ticks[i] + step * k;
			if ( t <= 0 || t >= fig.LengthTicks || ticks.Contains( t ) ) continue;
			if ( offPulse && DrumGroove.IsPulse( t ) ) continue;
			if ( !AllowedFigTick( t, fig, sk, role ) ) continue;
			// The VALUE and the VELOCITY move with the tick. A displace that moved only the tick
			// would keep the figure's cells and lose which hit was which — the chop that got pushed
			// would arrive wearing the next chop's articulation.
			int v = values[i]; float g = vels[i];
			ticks.RemoveAt( i ); values.RemoveAt( i ); vels.RemoveAt( i );
			int at = 0;
			while ( at < ticks.Count && ticks[at] < t ) at++;
			ticks.Insert( at, t ); values.Insert( at, v ); vels.Insert( at, g );
			return;
		}
	}

	/// <summary>RECOMBINE: take one bar of the phrase from another figure in the same genre's
	/// table. The genre's own vocabulary, re-cut — which is why this is the mutation that reaches
	/// furthest without ever producing a gesture the genre does not have.</summary>
	/// <param name="spine">Kept where the bar is cleared. Recombine is the one mutation that
	/// removes onsets it never looked at — it replaces a whole bar — so without this a genre's
	/// backbeat would survive Drop and Displace and then vanish anyway one time in six.</param>
	void Recombine( List<int> ticks, List<int> values, List<float> vels, Pattern fig, Rng rng,
		Pattern[] table, bool[] spine = null )
	{
		if ( table == null || table.Length < 2 ) return;
		Pattern other = null;
		for ( int tries = 0; tries < 4 && other == null; tries++ )
		{
			var p = table[rng.Int( table.Length )];
			if ( !ReferenceEquals( p, fig ) ) other = p;
		}
		if ( other == null ) return;

		int barTicks = _time.BarTicks;
		int bars = Math.Max( 1, fig.LengthTicks / barTicks );
		int bar = rng.Int( bars );
		int from = bar * barTicks, to = from + barTicks;

		for ( int i = ticks.Count - 1; i >= 0; i-- )
			if ( ticks[i] >= from && ticks[i] < to && (spine == null || !spine[i]) )
			{ ticks.RemoveAt( i ); values.RemoveAt( i ); vels.RemoveAt( i ); }

		// ONE bar of the other figure, folded onto this bar — and taken from ONE of its bars, not
		// every bar of it collapsed together. `tick % barTicks` maps a two-bar figure's second bar
		// back onto its first, so reading the whole thing would interleave two bars' onsets into
		// one and hand back a list that no longer ascends.
		int otherBar = (rng.Int( Math.Max( 1, other.LengthTicks / barTicks ) )) * barTicks;
		for ( int i = 0; i < other.Count; i++ )
		{
			int ot = other.TickAt( i );
			if ( ot < otherBar || ot >= otherBar + barTicks ) continue;
			int t = ot - otherBar + from;
			if ( t < from || t >= to || ticks.Contains( t ) ) continue;
			int at = 0;
			while ( at < ticks.Count && ticks[at] < t ) at++;
			ticks.Insert( at, t ); values.Insert( at, other.ValueAt( i ) ); vels.Insert( at, other.VelAt( i ) );
		}
	}
}
gamah.skafinity / Engine/CompFigure.cs
Game library
using System;

namespace Skafinity;

/// <summary>
/// The comp figures — what rhythm each genre's chordal voices actually play.
///
/// This was the loudest remaining duplication in the engine: one <c>KeysOnsets {0,3,4,7}</c>
/// served rock, country and pop, and one rhythm-guitar loop served rock, country and punk off a
/// single <c>country</c> bool. Three genres at a time played the SAME comping rhythm, whatever
/// harmony sat underneath — and the comp is most of what a listener hears as "the band".
///
/// A figure is a <see cref="Pattern"/>, so it owns its length: the ska horn answer is two bars
/// because it IS a call and response, the rock riff is two bars because a riff is a motif rather
/// than a bar, and punk is one bar because that is the whole idea of punk.
///
/// CELL VALUES say how the hit is played; <see cref="CompStyle"/> says what the voice does with
/// that. <see cref="Tone"/> cells name a single chord tone by index (for arpeggios and
/// alternating figures) instead of the whole voicing.
/// </summary>
static class CompFigure
{
	/// <summary>Full voicing, allowed to ring into the next hit.</summary>
	public const int Ring = 0;
	/// <summary>Full voicing, short — a stab or a chop.</summary>
	public const int Stab = 1;
	/// <summary>Root only, muted — the palm-muted chug between accents.</summary>
	public const int Mute = 2;
	/// <summary>A single chord tone: <c>Tone(0)</c> is the root, <c>Tone(1)</c> the next voice up.
	/// Wraps over the voicing, so an arpeggio can just count upward.</summary>
	public static int Tone( int i ) => 10 + i;
	public static bool IsTone( int v ) => v >= 10;
	public static int ToneIndex( int v ) => v - 10;

	const int R = Harmony.Rest;

	static Pattern E( params int[] cells ) => Pattern.Eighths( cells );
	static Pattern S( params int[] cells ) => Pattern.Sixteenths( cells );
	static Pattern T( params int[] cells ) => Pattern.ThirtySeconds( cells );

	// ── Ska-punk: the skank chop on the offbeats. The second figure answers itself across two bars
	// (the push on the "and of 4" pulls into bar 2), which one-bar tables could not express.
	public static readonly Pattern[] SkaPunk =
	{
		E( R, Stab, R, Stab, R, Stab, R, Stab ),
		E( R, Stab, R, Stab, R, Stab, R, Stab,
		   R, Stab, R, Stab, R, Stab, Stab, Stab ),
		E( R, Stab, R, Stab, R, Stab, R, R,
		   R, Stab, R, Stab, R, Stab, R, Stab ),
	};

	// The flick: the last chop of the two-bar phrase doubles at the THIRTY-SECOND, so the
	// skank pushes into the next bar instead of just stopping. Two notes off a wrist that is
	// already moving — the same hand that plays the chop plays the flick.
	public static readonly Pattern SkaPunkFlick =
		T( R,R,R,R, Stab,R,R,R, R,R,R,R, Stab,R,R,R,
		   R,R,R,R, Stab,R,R,R, R,R,R,R, Stab,R,R,R,
		   R,R,R,R, Stab,R,R,R, R,R,R,R, Stab,R,R,R,
		   R,R,R,R, Stab,R,R,R, R,R,R,R, Stab,Stab,R,R );

	// ── Ska-punk (loud): what the same voice plays once the section is loud — see GenreProfile.LoudComp.
	// The skank stops. These are the guitar part of a third-wave chorus: on the beat, ringing, and
	// through a driven amp (so RhythmGtrTone's drive takes the third out and they land as power
	// chords). The offbeat does not vanish from the song — the horns and the kit still carry it,
	// which is what keeps a loud ska chorus from simply being a punk chorus.
	public static readonly Pattern[] SkaPunkLoud =
	{
		E( Ring, R, Stab, R, Ring, R, Stab, R ),
		// Two bars, the second pushing back into the first: the chorus figure that answers itself.
		E( Ring, R, R, Stab, Ring, R, Stab, R,
		   Ring, R, R, Stab, Ring, Stab, Stab, R ),
		// The one that keeps the offbeat inside the loud part — downbeat power chord, offbeat
		// answer. This is the figure that still sounds like ska with the gain on.
		E( Ring, Stab, R, Stab, Ring, Stab, R, Stab ),
	};

	// ── Rock: a real two-bar riff motif. NOT an every-eighth chug — the hits are placed, they
	// ring, and the second bar answers the first.
	public static readonly Pattern[] Rock =
	{
		E( Ring, R, R, Stab, Ring, R, Stab, R,
		   Ring, R, R, Stab, Ring, R, Stab, Stab ),
		E( Ring, R, Stab, R, R, Ring, R, Stab,
		   Ring, R, Stab, R, R, Ring, Stab, R ),
		E( Ring, Mute, Mute, Ring, R, Mute, Ring, R,
		   Ring, Mute, Mute, Ring, R, Ring, R, Stab ),
	};

	// The first figure with a THIRTY-SECOND pickup pushing the phrase back round to bar 1 — a
	// riff's pickup is the oldest ornament in rock guitar. Note it REPLACES the last stab
	// rather than joining it: an ornament that adds notes makes the comp louder as well as
	// busier, and the comp is the bed (see the mix balance in the engine suite).
	public static readonly Pattern RockPickup =
		T( Ring,R,R,R, R,R,R,R, R,R,R,R, Stab,R,R,R,
		   Ring,R,R,R, R,R,R,R, Stab,R,R,R, R,R,R,R,
		   Ring,R,R,R, R,R,R,R, R,R,R,R, Stab,R,R,R,
		   Ring,R,R,R, R,R,R,R, Stab,R,R,R, R,R,Stab,Stab );

	// ── Rock keys: the syncopated Charleston push. Kept as its own voice with its own figure so
	// the two interlock rather than doubling.
	public static readonly Pattern[] RockKeys =
	{
		E( Ring, R, R, Stab, Ring, R, R, Stab ),
		E( Ring, R, R, Stab, R, Ring, R, R,
		   R, Stab, Ring, R, R, Stab, R, R ),
	};

	// ── Country: the "chick" — a clean strum on every offbeat, over the bass's "boom". This is
	// the half of boom-chick the guitar owns; the bass tables own the other half.
	public static readonly Pattern[] Country =
	{
		E( R, Stab, R, Stab, R, Stab, R, Stab ),
		E( R, Stab, R, Stab, R, Stab, R, Stab,
		   R, Stab, R, Stab, R, Stab, Stab, R ),
	};

	// Chicken-pickin': a chick snaps a THIRTY-SECOND pull-off after it — one plucked note and
	// one that costs the picking hand nothing, which is why the gesture survives at country's
	// fastest and its slowest alike. TWICE in two bars, not on every chick: a second note on
	// every one of them is a different instrument rather than an ornament, which is the same
	// lesson the lead's double-stops learned.
	public static readonly Pattern CountryPickOff =
		T( R,R,R,R, Stab,R,R,R, R,R,R,R, Stab,R,R,R,
		   R,R,R,R, Stab,R,R,R, R,R,R,R, Stab,R,R,R,
		   R,R,R,R, Stab,R,R,R, R,R,R,R, Stab,Stab,R,R,
		   R,R,R,R, Stab,R,R,R, R,R,R,R, Stab,Stab,R,R );

	// ── Country keys: honky-tonk piano stabs on 2 and 4 — it answers the backbeat rather than
	// keeping time, which is what stops it from doubling the guitar.
	public static readonly Pattern[] CountryKeys =
	{
		E( R, R, Stab, R, R, R, Stab, R ),
		E( R, R, Stab, R, R, R, Stab, Stab,
		   R, R, Stab, R, Stab, R, Stab, R ),
	};

	// ── Punk: downstroke eighths, one chord per bar, nothing else. The variation is that the
	// four-bar phrase drops a hit at the end to breathe before the turnaround.
	public static readonly Pattern[] Punk =
	{
		E( Ring, Ring, Ring, Ring, Ring, Ring, Ring, Ring ),
		E( Ring, Ring, Ring, Ring, Ring, Ring, Ring, Ring,
		   Ring, Ring, Ring, Ring, Ring, Ring, Ring, Ring,
		   Ring, Ring, Ring, Ring, Ring, Ring, Ring, Ring,
		   Ring, Ring, Ring, Ring, Ring, Ring, R, Ring ),
	};

	// The turnaround flurry: two bars of downstrokes, and the last eighth breaks into
	// THIRTY-SECONDS on the way back round. Punk's whole idea is that nothing lets up, so the
	// ornament is where it hands over, not scattered through the bar.
	public static readonly Pattern PunkTurnaround =
		T( Ring,R,R,R, Ring,R,R,R, Ring,R,R,R, Ring,R,R,R,
		   Ring,R,R,R, Ring,R,R,R, Ring,R,R,R, Ring,R,R,R,
		   Ring,R,R,R, Ring,R,R,R, Ring,R,R,R, Ring,R,R,R,
		   Ring,R,R,R, Ring,R,R,R, Ring,R,R,R, Ring,Ring,Ring,Ring );

	// ── Metal: the palm-muted gallop, authored at the sixteenth it actually lives on. Ring hits
	// are the power-chord accents; everything between them is the muted root.
	public static readonly Pattern[] Metal =
	{
		S( Ring, Mute, Mute, Mute, Ring, Mute, Mute, Mute,
		   Ring, Mute, Mute, Mute, Ring, Mute, Mute, Mute ),
		S( Ring, Mute, Mute, Ring, Mute, Mute, Ring, Mute,
		   Mute, Ring, Mute, Mute, Ring, Mute, Mute, Mute,
		   Ring, Mute, Mute, Ring, Mute, Mute, Ring, Mute,
		   Mute, Ring, Mute, Mute, Ring, Ring, Mute, Mute ),   // the classic "gallop" (2 bars)
		S( Ring, R, Mute, Mute, Ring, R, Mute, Mute,
		   Ring, R, Mute, Mute, Ring, Ring, R, R ),
	};

	// Authored at the THIRTY-SECOND so the chug can burst. Most of the bar is the same
	// sixteenth chug as the figures above (a cell, then a rest cell); the last beat runs
	// 32nds into the bar line. That is the tremolo gesture as a player uses it — a flurry
	// pulling into the next downbeat — rather than a bar of it, and being a handful of notes
	// it stays a gesture at the top of the genre's band as much as the bottom.
	public static readonly Pattern MetalTremolo =
		T( Ring, R, Mute, R, Mute, R, Mute, R,
		   Ring, R, Mute, R, Mute, R, Mute, R,
		   Ring, R, Mute, R, Mute, R, Mute, R,
		   Ring, R, Mute, R, Mute, Mute, Mute, Mute );

	// ── Pop: a held pad. One hit a bar, ringing the whole way — the harmony is a bed here, not a
	// rhythm part, which is exactly what the arp on top needs.
	public static readonly Pattern[] Pop =
	{
		E( Ring, R, R, R, R, R, R, R ),
		E( Ring, R, R, R, R, R, R, R,
		   Ring, R, R, R, R, R, Ring, R ),
	};

	// ── Pop arp: sixteenths climbing the voicing. The tone indices wrap over whatever voicing
	// the song drew, so an add9 arp reaches the 9th without the figure knowing what a 9th is.
	public static readonly Pattern[] PopArp =
	{
		S( Tone( 0 ), Tone( 1 ), Tone( 2 ), Tone( 3 ), Tone( 2 ), Tone( 1 ), Tone( 2 ), Tone( 3 ),
		   Tone( 0 ), Tone( 1 ), Tone( 2 ), Tone( 3 ), Tone( 2 ), Tone( 1 ), Tone( 2 ), Tone( 1 ) ),
		S( Tone( 0 ), R, Tone( 2 ), Tone( 1 ), Tone( 0 ), R, Tone( 2 ), Tone( 3 ),
		   Tone( 0 ), R, Tone( 2 ), Tone( 1 ), Tone( 3 ), Tone( 2 ), Tone( 1 ), Tone( 0 ) ),
	};

	// The sixteenth arp with a THIRTY-SECOND run home over the last beat — the synth-pop
	// flourish that resolves the two-bar phrase. A sequencer plays it and so does a keyboard
	// player; either way it is one beat of the figure, not the figure's rate.
	public static readonly Pattern PopArpRun =
		T( Tone(0),R, Tone(1),R, Tone(2),R, Tone(3),R,
		   Tone(2),R, Tone(1),R, Tone(2),R, Tone(3),R,
		   Tone(0),R, Tone(1),R, Tone(2),R, Tone(3),R,
		   Tone(2),R, Tone(1),R, Tone(3),Tone(2), Tone(1),Tone(0) );

	// ── Hemiola: the cadential regrouping. Three eighths long, so it does NOT divide the bar —
	// the figure and the bar line pull apart and re-converge, which is Biamonte's grouping
	// dissonance and the reason Pattern carries its own length at all. Any chordal voice can
	// swap to this for the last bars of a section (see MusicGen.RenderSection).
	public static readonly Pattern Hemiola = E( Stab, R, Stab );
}
gamah.skafinity / Engine/VibeCodec.cs
Game library
using System;
using System.Collections.Generic;
using System.Text;

namespace Skafinity;

/// <summary>
/// The knob grid, and its compact hex encoding — the "vibe" half of a seed
/// (<c>tag:n[:genre][:vibe]</c>; the string as a whole is <see cref="SeedCodec"/>'s).
///
/// WIRE FORMAT — one GLOBAL grid, genre-independent and fixed width:
///   <c>[voice 0 cols 1..4][voice 1 cols 1..4]…</c>, one hex digit per cell,
///   <see cref="VoiceCount"/> × <see cref="WireColumns"/> = <see cref="VibeLength"/> chars.
///
/// An instrument sits at the SAME index in every genre, whether or not that genre plays it, and
/// every cell is a fixed (Config field, range) pair — <see cref="Cells"/> — that no genre may
/// redefine. That is what makes a vibe portable: pin one, let the genre roll, and each song reads
/// the same 36 numbers through whatever voices it happens to use. A genre chooses which cells it
/// EXPOSES as sliders and what to call them (<see cref="GenreDef"/>), and nothing else.
///
/// The wire carries a NORMALISED level (0..15 over the cell's range), not a raw value, so a
/// genre's character comes from its voice code and <see cref="GenreProfile"/> — the places that
/// already hold it — rather than from a per-genre range on the knob.
///
/// A vibe is EXACTLY <see cref="VibeLength"/> hex chars. Short, long or non-hex is not a vibe;
/// there is no pad-with-defaults degrade, because a half-read grid is a song nobody chose. Growing
/// the grid (a voice, a 5th column) changes that length and invalidates every shared vibe: it is a
/// format break, not an append. Volume never travels — it is column 0, a local mix preference.
///
/// Lossy by design (16 levels/cell) but stable: Encode(Apply(s)) == s for any valid s.
/// </summary>
public static class VibeCodec
{
	internal const string Hex = "0123456789abcdef";
	public const int Levels = 16;       // one hex digit per knob
	public const int Columns = 5;       // 0 volume, then four travelling columns
	/// <summary>First column that travels. Column 0 is VOLUME — a local mix preference
	/// (see <see cref="ReadVolumes"/>), so the whole column is skipped rather than encoded.</summary>
	public const int WireFirstColumn = 1;
	public const int WireColumns = Columns - WireFirstColumn;

	public sealed class Field
	{
		public string Name;
		public float Min, Max;
		public bool Int;
		/// <summary>Discrete option labels (value = Min + index); null for a continuous knob.</summary>
		public string[] Choices;
		public Func<MusicGen.Config, float> Get;
		public Action<MusicGen.Config, float> Set;
		/// <summary>Instrument row this knob belongs to.</summary>
		public string Voice;
		/// <summary>Matrix column: 0 volume, 1..4 the travelling columns.</summary>
		public int Column;

		/// <summary>Current value as a 0..1 fraction of the range.</summary>
		public float GetNorm( MusicGen.Config c ) =>
			Math.Clamp( (Get( c ) - Min) / (Max - Min), 0f, 1f );

		/// <summary>Set from a 0..1 fraction (rounded for integer/discrete knobs).</summary>
		public void SetNorm( MusicGen.Config c, float norm )
		{
			float v = Min + Math.Clamp( norm, 0f, 1f ) * (Max - Min);
			if ( Int || Choices != null ) v = (float)Math.Round( v );
			Set( c, v );
		}

		/// <summary>Human-readable current value for the row header.</summary>
		public string Display( MusicGen.Config c )
		{
			float v = Get( c );
			if ( Choices != null )
			{
				int idx = (int)Math.Clamp( Math.Round( v - Min ), 0, Choices.Length - 1 );
				return Choices[idx];
			}
			if ( Int ) return ((int)Math.Round( v )).ToString();
			// A knob whose whole range fits in 0..2 is a proportion, not a count — rounding it to
			// a whole number shows the same "1" across most of its travel. Read those as percents
			// (a 0..1.5 volume, a 0.7..1.45 tempo scale); anything wider is a real quantity (Hz,
			// cents, a drive amount) and stays a number.
			if ( Max <= 2f ) return $"{(int)Math.Round( v * 100 )}%";
			return ((int)Math.Round( v )).ToString();
		}
	}

	static Field F( string name, float min, float max, bool isInt,
		Func<MusicGen.Config, float> get, Action<MusicGen.Config, float> set,
		string voice, int column, string[] choices = null )
		=> new() { Name = name, Min = min, Max = max, Int = isInt, Get = get, Set = set,
			Voice = voice, Column = column, Choices = choices };

	// ── The global voice table ────────────────────────────────────────────────────────────
	// Identity is the VOICE, not the label a genre puts on it: ska's "LEAD" and pop's "LEAD" are
	// two different voices (MELODY and LEAD GTR), and pop's "SYNTH" is rock's KEYS. Index order is
	// the wire order and is fixed; a genre's display order is its own (see GenreDef.Rows).
	public const int VoiceMelody = 4;

	sealed class VoiceDef
	{
		public string Name;
		public Field Volume;        // column 0 — never on the wire
		public Field[] Cells;       // columns 1..4, null where the grid has no knob at all
	}

	static VoiceDef V( string name, Func<MusicGen.Config, float> volGet, Action<MusicGen.Config, float> volSet,
		Field c1, Field c2, Field c3, Field c4 )
		=> new() { Name = name, Volume = F( "VOLUME", 0f, 1.5f, false, volGet, volSet, name, 0 ),
			Cells = new[] { c1, c2, c3, c4 } };

	// The cell table. A cell's Config field and RANGE are global — the same 16 levels mean the same
	// thing in every genre, which is what a portable vibe requires. Where a genre wants a different
	// floor or a different amount of an effect, that already lives in its voice code (Guitar.cs and
	// Lead.cs offset the drive per genre) or in GenreProfile; it must not come back here as a
	// per-genre range, or a pinned vibe stops meaning one thing.
	static readonly VoiceDef[] Voices =
	{
		V( "DRUMS", c => c.DrumVol, ( c, v ) => c.DrumVol = v,
			F( "TONE", 0f, 1f, false, c => c.DrumTone, ( c, v ) => c.DrumTone = v, "DRUMS", 1 ),
			F( "BUSY", 0f, 1f, false, c => c.DrumBusy, ( c, v ) => c.DrumBusy = v, "DRUMS", 2 ),
			F( "DRIVE", 0f, 1f, false, c => c.DrumDrive, ( c, v ) => c.DrumDrive = v, "DRUMS", 3 ),
			null ),
		V( "BASS", c => c.BassVol, ( c, v ) => c.BassVol = v,
			F( "TONE", 80f, 1200f, false, c => c.BassCutoff, ( c, v ) => c.BassCutoff = v, "BASS", 1 ),
			F( "DRIVE", 1f, 4f, false, c => c.BassDrive, ( c, v ) => c.BassDrive = v, "BASS", 2 ),
			F( "OCTAVE POP", 0f, 1f, false, c => c.OctavePopChance, ( c, v ) => c.OctavePopChance = v, "BASS", 3 ),
			F( "TRIPLETS", 0f, 0.1f, false, c => c.BassTriplets, ( c, v ) => c.BassTriplets = v, "BASS", 4 ) ),
		V( "SKANK", c => c.SkankVol, ( c, v ) => c.SkankVol = v,
			F( "TONE", 500f, 8000f, false, c => c.SkankCutoff, ( c, v ) => c.SkankCutoff = v, "SKANK", 1 ),
			F( "BITE", 0f, 2000f, false, c => c.SkankHighpass, ( c, v ) => c.SkankHighpass = v, "SKANK", 2 ),
			F( "CHOP", 0.15f, 1f, false, c => c.SkankChop, ( c, v ) => c.SkankChop = v, "SKANK", 3 ),
			null ),
		V( "ORGAN", c => c.OrganVol, ( c, v ) => c.OrganVol = v,
			F( "TONE", 500f, 8000f, false, c => c.OrganCutoff, ( c, v ) => c.OrganCutoff = v, "ORGAN", 1 ),
			F( "BUBBLE", 0f, 1f, false, c => c.OrganBubbleChance, ( c, v ) => c.OrganBubbleChance = v, "ORGAN", 2 ),
			F( "VIBRATO", 0f, 12f, false, c => c.OrganVibrato, ( c, v ) => c.OrganVibrato = v, "ORGAN", 3 ),
			null ),
		V( "MELODY", c => c.MelodyVol, ( c, v ) => c.MelodyVol = v,
			F( "TONE", 500f, 8000f, false, c => c.LeadCutoff, ( c, v ) => c.LeadCutoff = v, "MELODY", 1 ),
			F( "JUMPINESS", 0f, 1f, false, c => c.MelodyLeapChance, ( c, v ) => c.MelodyLeapChance = v, "MELODY", 2 ),
			F( "TRIPLETS", 0f, 0.1f, false, c => c.TripletChance, ( c, v ) => c.TripletChance = v, "MELODY", 3 ),
			null ),
		V( "HORNS", c => c.HornVol, ( c, v ) => c.HornVol = v,
			F( "TONE", 500f, 8000f, false, c => c.HornCutoff, ( c, v ) => c.HornCutoff = v, "HORNS", 1 ),
			F( "SECTION", 0f, 1f, false, c => c.HornSectionChance, ( c, v ) => c.HornSectionChance = v, "HORNS", 2 ),
			F( "DENSITY", 0f, 1f, false, c => c.HornDensity, ( c, v ) => c.HornDensity = v, "HORNS", 3 ),
			null ),
		V( "KEYS", c => c.KeysVol, ( c, v ) => c.KeysVol = v,
			F( "TONE", 500f, 8000f, false, c => c.KeysCutoff, ( c, v ) => c.KeysCutoff = v, "KEYS", 1 ),
			F( "DISTORTION", 1f, 5f, false, c => c.KeysDrive, ( c, v ) => c.KeysDrive = v, "KEYS", 2 ),
			F( "CHUG", 0f, 1f, false, c => c.KeysChug, ( c, v ) => c.KeysChug = v, "KEYS", 3 ),
			null ),
		V( "RHYTHM GTR", c => c.RhythmGtrVol, ( c, v ) => c.RhythmGtrVol = v,
			F( "TONE", 500f, 8000f, false, c => c.RhythmGtrCutoff, ( c, v ) => c.RhythmGtrCutoff = v, "RHYTHM GTR", 1 ),
			F( "DISTORTION", 1f, 6f, false, c => c.RhythmGtrDrive, ( c, v ) => c.RhythmGtrDrive = v, "RHYTHM GTR", 2 ),
			F( "CHUG", 0f, 1f, false, c => c.RhythmGtrChug, ( c, v ) => c.RhythmGtrChug = v, "RHYTHM GTR", 3 ),
			null ),
		V( "LEAD GTR", c => c.LeadGtrVol, ( c, v ) => c.LeadGtrVol = v,
			F( "TONE", 500f, 8000f, false, c => c.LeadGtrCutoff, ( c, v ) => c.LeadGtrCutoff = v, "LEAD GTR", 1 ),
			F( "DISTORTION", 1f, 6f, false, c => c.LeadGtrDrive, ( c, v ) => c.LeadGtrDrive = v, "LEAD GTR", 2 ),
			F( "BENDINESS", 0f, 1f, false, c => c.LeadGtrBend, ( c, v ) => c.LeadGtrBend = v, "LEAD GTR", 3 ),
			null ),
	};

	public static int VoiceCount => Voices.Length;
	/// <summary>Exact length of a vibe string, derived from the grid so nothing restates it.</summary>
	public static readonly int VibeLength = Voices.Length * WireColumns;

	/// <summary>The wire position of voice <paramref name="v"/>'s column <paramref name="col"/>.</summary>
	static int Pos( int v, int col ) => v * WireColumns + (col - WireFirstColumn);

	/// <summary>Is there a knob at wire position <paramref name="pos"/>? The grid is rectangular, so
	/// some cells are holes — a voice with three columns still reserves its fourth. A hole encodes
	/// as '0' and decodes to nothing, and a roll must leave it at '0' too: anything else would
	/// re-encode to '0' and make a rolled vibe fail to round-trip.</summary>
	public static bool HasCell( int pos )
	{
		if ( pos < 0 || pos >= VibeLength ) return false;
		return Voices[pos / WireColumns].Cells[pos % WireColumns] != null;
	}

	// ── Advanced / tuning-only knobs ──
	// Config fields that shape the BASELINE MIX (peak balances, kit presence) rather than a
	// song's shareable identity. They are NOT in the vibe wire (Encode/Apply never touch them)
	// and NOT in Fields() (so they don't appear as per-genre sliders). Membership in THIS list
	// is exactly the "config value, not a vibe slider" marker. Surfaced to the host (web:
	// config.json) by NAME — names match the MusicGen.Config field 1:1 — so the house mix can
	// be retuned at runtime without a rebuild. Ranges are generous tuning bounds, not the seed
	// grid. Genre-independent; not positional, so nothing here can shift a wire cell.
	public static readonly Field[] AdvancedFields =
	{
		F( "KitPresence", 0f, 4f, false, c => c.KitPresence, ( c, v ) => c.KitPresence = v, null, 0 ),
		// The stereo image, and the house's scale over each song's drawn reverb. Both were vibe
		// sliders; both are environment rather than music. 1 = as designed.
		F( "PanAmount", 0f, 1f, false, c => c.PanAmount, ( c, v ) => c.PanAmount = v, null, 0 ),
		F( "MasterReverb", 0f, 2f, false, c => c.MasterReverb, ( c, v ) => c.MasterReverb = v, null, 0 ),
		// How far each genre's own mix profile (GenreProfile.Mix) is taken. 1 = as designed,
		// 0 = every genre through one neutral mix. The SHAPE of a genre's mix is character and
		// lives in the profile; what the house retunes at runtime is how far to push it.
		F( "GenreMix", 0f, 2f, false, c => c.GenreMix, ( c, v ) => c.GenreMix = v, null, 0 ),
		F( "KickBalance", 0f, 2f, false, c => c.KickBalance, ( c, v ) => c.KickBalance = v, null, 0 ),
		F( "SnareBalance", 0f, 2f, false, c => c.SnareBalance, ( c, v ) => c.SnareBalance = v, null, 0 ),
		F( "TomBalance", 0f, 2f, false, c => c.TomBalance, ( c, v ) => c.TomBalance = v, null, 0 ),
		F( "HatBalance", 0f, 2f, false, c => c.HatBalance, ( c, v ) => c.HatBalance = v, null, 0 ),
		F( "RideBalance", 0f, 2f, false, c => c.RideBalance, ( c, v ) => c.RideBalance = v, null, 0 ),
		F( "CrashBalance", 0f, 2f, false, c => c.CrashBalance, ( c, v ) => c.CrashBalance = v, null, 0 ),
		F( "BassBalance", 0f, 2f, false, c => c.BassBalance, ( c, v ) => c.BassBalance = v, null, 0 ),
		F( "SkankBalance", 0f, 2f, false, c => c.SkankBalance, ( c, v ) => c.SkankBalance = v, null, 0 ),
		F( "OrganBalance", 0f, 2f, false, c => c.OrganBalance, ( c, v ) => c.OrganBalance = v, null, 0 ),
		F( "MelodyBalance", 0f, 2f, false, c => c.MelodyBalance, ( c, v ) => c.MelodyBalance = v, null, 0 ),
		F( "HornBalance", 0f, 2f, false, c => c.HornBalance, ( c, v ) => c.HornBalance = v, null, 0 ),
		F( "KeysBalance", 0f, 2f, false, c => c.KeysBalance, ( c, v ) => c.KeysBalance = v, null, 0 ),
		F( "RhythmGtrBalance", 0f, 2f, false, c => c.RhythmGtrBalance, ( c, v ) => c.RhythmGtrBalance = v, null, 0 ),
		F( "LeadGtrBalance", 0f, 2f, false, c => c.LeadGtrBalance, ( c, v ) => c.LeadGtrBalance = v, null, 0 ),
		// Stereo double-tracking / width (see MusicGen.Config "width" block).
		F( "DoubleTrack", 0f, 1f, false, c => c.DoubleTrack, ( c, v ) => c.DoubleTrack = v, null, 0 ),
		F( "WidthBacking", 0f, 1f, false, c => c.WidthBacking, ( c, v ) => c.WidthBacking = v, null, 0 ),
		F( "WidthLead", 0f, 1f, false, c => c.WidthLead, ( c, v ) => c.WidthLead = v, null, 0 ),
		// Bounded at 20 cents, not 50: half a quarter-tone between two takes is not a double, it is
		// a tuning error, and this is a house-config field with no way for a listener to undo it.
		F( "WidthDetune", 0f, 20f, false, c => c.WidthDetune, ( c, v ) => c.WidthDetune = v, null, 0 ),
		F( "WidthDelayMs", 0f, 40f, false, c => c.WidthDelayMs, ( c, v ) => c.WidthDelayMs = v, null, 0 ),
		F( "WidthJitterMs", 0f, 30f, false, c => c.WidthJitterMs, ( c, v ) => c.WidthJitterMs = v, null, 0 ),
		F( "WidthAmpVar", 0f, 1f, false, c => c.WidthAmpVar, ( c, v ) => c.WidthAmpVar = v, null, 0 ),
		F( "WidthCutoffVar", 0f, 1f, false, c => c.WidthCutoffVar, ( c, v ) => c.WidthCutoffVar = v, null, 0 ),
	};

	/// <summary>Overlay a <c>name → raw value</c> map (the shared config file's "advanced" block)
	/// onto <paramref name="c"/>. Keys match <see cref="AdvancedFields"/> names (= Config field
	/// names) 1:1; unknown keys are ignored and values are clamped to each field's range. Both
	/// hosts use this: s&box reads the file and calls this; the web mirrors it in JS over the
	/// same field list. Call it where the baseline mix is assembled (after defaults/vibe).</summary>
	public static void ApplyAdvanced( IReadOnlyDictionary<string, float> values, MusicGen.Config c )
	{
		if ( c == null || values == null ) return;
		foreach ( var f in AdvancedFields )
			if ( values.TryGetValue( f.Name, out var v ) )
				f.Set( c, Math.Clamp( v, f.Min, f.Max ) );
	}

	// ── Genres: which cells they expose, in what order, under what name ───────────────────
	// A genre NEVER redefines what a cell does — it picks the rows a listener gets sliders for and
	// the words on them. Everything a genre sounds like is in its voice code and GenreProfile.
	sealed class RowDef
	{
		public int Voice;                 // index into Voices
		public string Label;              // what this genre calls it (null = the voice's own name)
		public int[] Columns;             // which travelling columns it exposes
		public string[] Labels;           // per-column name override (null entry = the cell's own)
	}

	sealed class GenreDef
	{
		public string Name;
		public RowDef[] Rows;             // display order
	}

	// All four cells of a voice, under the voice's own names. The common case.
	static RowDef R( int voice, string label = null )
		=> new() { Voice = voice, Label = label, Columns = null, Labels = null };

	// A subset of a voice's cells, optionally renamed: R( V, "SYNTH", (1, null), (3, "PLUCK") ).
	static RowDef R( int voice, string label, params (int col, string name)[] cells )
	{
		var cols = new int[cells.Length];
		var names = new string[cells.Length];
		for ( int i = 0; i < cells.Length; i++ ) { cols[i] = cells[i].col; names[i] = cells[i].name; }
		return new RowDef { Voice = voice, Label = label, Columns = cols, Labels = names };
	}

	const int Drums = 0, Bass = 1, Skank = 2, Organ = 3, Melody = 4, Horns = 5,
		Keys = 6, RhythmGtr = 7, LeadGtr = 8;

	static readonly GenreDef[] GenreDefs =
	{
		// Ska-Punk. The chorus guitar is the RHYTHM GTR voice: third-wave ska drops the skank for
		// driven power chords once the section is loud (GenreProfile.LoudComp).
		new() { Name = "Ska-Punk", Rows = new[]
		{
			R( Bass ), R( Skank ), R( Organ ), R( Melody, "LEAD" ), R( Horns ), R( Drums ),
			R( RhythmGtr, "CHORUS GTR" ),
		} },
		new() { Name = "Rock", Rows = new[]
		{
			R( Drums ), R( Bass ), R( Keys ), R( LeadGtr ), R( RhythmGtr ),
		} },
		// Country — clean strummed open chords, honky-tonk piano, twangy telecaster lead. The
		// cleaner floor under each DISTORTION knob is in Guitar.cs / Lead.cs / Keys.cs.
		new() { Name = "Country", Rows = new[]
		{
			R( Drums ), R( Bass ), R( RhythmGtr ), R( Keys ), R( LeadGtr ),
		} },
		new() { Name = "Metal", Rows = new[]
		{
			R( Drums ), R( Bass ), R( RhythmGtr ), R( LeadGtr ),
		} },
		// Punk — "lean punk" / power-pop: rock's voices without the keys.
		new() { Name = "Punk", Rows = new[]
		{
			R( Drums ), R( Bass ), R( RhythmGtr ), R( LeadGtr ),
		} },
		// Pop — modern synth/dance-pop. The KEYS voice run clean and bright (PLUCK tightens the
		// ringing pad toward stabs) and the LEAD GTR voice run clean as a plucky synth lead. Both
		// hide their DISTORTION cell: pop's clean floor is Keys.cs / Lead.cs, and a slider that
		// the voice code overrules is a lie. The cell still TRAVELS — every vibe is full width.
		new() { Name = "Pop", Rows = new[]
		{
			R( Drums ), R( Bass ),
			R( Keys, "SYNTH", (1, null), (3, "PLUCK") ),
			R( LeadGtr, "LEAD", (1, null), (3, "GLIDE") ),
		} },
	};

	public static int GenreCount => GenreDefs.Length;
	public static IReadOnlyList<string> Genres
	{
		get { var a = new string[GenreDefs.Length]; for ( int i = 0; i < a.Length; i++ ) a[i] = GenreDefs[i].Name; return a; }
	}

	static GenreDef Def( int genre ) => GenreDefs[Math.Clamp( genre, 0, GenreDefs.Length - 1 )];

	/// <summary>A genre's row as the UI shows it: the voice's field, relabelled where the genre
	/// says so. The returned Field is a copy — the global cell is never mutated.</summary>
	static Field Labelled( Field cell, string voiceLabel, string nameOverride )
	{
		if ( cell == null ) return null;
		if ( voiceLabel == null && nameOverride == null ) return cell;
		return new Field
		{
			Name = nameOverride ?? cell.Name, Min = cell.Min, Max = cell.Max, Int = cell.Int,
			Choices = cell.Choices, Get = cell.Get, Set = cell.Set,
			Voice = voiceLabel ?? cell.Voice, Column = cell.Column,
		};
	}

	/// <summary>Flat list of the sliders <paramref name="genre"/> shows, in display order: each
	/// row's volume then its exposed cells. Each field carries its <see cref="Field.Voice"/> /
	/// <see cref="Field.Column"/>, so the UI lays the matrix out without a second table.</summary>
	/// <remarks>The returned fields are STABLE: the same genre hands back the same objects every
	/// call, because a UI identifies a knob by reference to find its wire index. <see cref="Labelled"/>
	/// mints a fresh <see cref="Field"/> for any row its genre renames, so a list rebuilt per call
	/// makes those knobs unfindable — they draw and drag and set nothing, and only on the genres that
	/// rename a row. Cached per genre for that reason first and for the allocations second.</remarks>
	public static IReadOnlyList<Field> Fields( int genre )
	{
		int g = Math.Clamp( genre, 0, GenreDefs.Length - 1 );
		var cache = _fields ??= new IReadOnlyList<Field>[GenreDefs.Length];
		if ( cache[g] != null ) return cache[g];

		var list = new List<Field>();
		foreach ( var row in GenreDefs[g].Rows )
		{
			var v = Voices[row.Voice];
			list.Add( Labelled( v.Volume, row.Label, null ) );
			if ( row.Columns == null )
			{
				foreach ( var cell in v.Cells )
					if ( cell != null ) list.Add( Labelled( cell, row.Label, null ) );
			}
			else
			{
				for ( int i = 0; i < row.Columns.Length; i++ )
				{
					var cell = v.Cells[row.Columns[i] - WireFirstColumn];
					if ( cell != null ) list.Add( Labelled( cell, row.Label, row.Labels[i] ) );
				}
			}
		}
		cache[g] = list;
		return list;
	}

	// One list per genre, built on first ask. Lazy rather than a static initialiser so it cannot
	// depend on field-initialisation order with GenreDefs.
	static IReadOnlyList<Field>[] _fields;

	/// <summary>True if <paramref name="f"/> is a per-instrument VOLUME knob — column 0 of an
	/// instrument row, kept out of the shareable seed and persisted per-voice instead.</summary>
	public static bool IsVolume( Field f ) => f != null && f.Voice != null && f.Column == 0;

	/// <summary>Read the per-instrument volumes of <paramref name="genre"/> off
	/// <paramref name="c"/> as a <c>voice → 0..1 level</c> map. The key is the label this genre
	/// uses, which is what the UI shows; a voice a genre renames (pop's "SYNTH") therefore keeps
	/// its own level there. Merge this into a single store across genres.</summary>
	public static Dictionary<string, float> ReadVolumes( int genre, MusicGen.Config c )
	{
		var d = new Dictionary<string, float>();
		if ( c == null ) return d;
		foreach ( var f in Fields( genre ) )
			if ( IsVolume( f ) ) d[f.Voice] = f.GetNorm( c );
		return d;
	}

	/// <summary>Overlay a <c>voice → 0..1 level</c> map (from <see cref="ReadVolumes"/> / storage)
	/// onto <paramref name="c"/> for <paramref name="genre"/>. Voices absent from the map keep
	/// their current/default level. Call this after <see cref="Apply"/> so a song's saved mix
	/// rides on top of the seed's voicing.</summary>
	public static void ApplyVolumes( int genre, IReadOnlyDictionary<string, float> vols, MusicGen.Config c )
	{
		if ( c == null || vols == null ) return;
		foreach ( var f in Fields( genre ) )
			if ( IsVolume( f ) && vols.TryGetValue( f.Voice, out var n ) )
				f.SetNorm( c, n );
	}

	// ── The wire ──────────────────────────────────────────────────────────────────────────

	/// <summary>Encode the whole global grid off <paramref name="c"/>. Genre-independent: the
	/// result depends on the knobs and on nothing else, so re-encoding after a genre change hands
	/// back the same string.</summary>
	public static string Encode( MusicGen.Config c )
	{
		if ( c == null ) return "";
		var sb = new StringBuilder( VibeLength );
		foreach ( var v in Voices )
			foreach ( var cell in v.Cells )
				sb.Append( cell != null ? Quant( cell, c ) : Hex[0] );
		return sb.ToString();
	}

	static char Quant( Field f, MusicGen.Config c )
	{
		int q = (int)Math.Round( f.GetNorm( c ) * (Levels - 1) );
		return Hex[Math.Clamp( q, 0, Levels - 1 )];
	}

	/// <summary>True if <paramref name="s"/> is a vibe: exactly <see cref="VibeLength"/> hex
	/// chars. There is no near-miss — see the class remarks on why short does not degrade.</summary>
	public static bool IsVibe( string s )
	{
		if ( s == null || s.Length != VibeLength ) return false;
		foreach ( var ch in s )
			if ( Hex.IndexOf( char.ToLowerInvariant( ch ) ) < 0 ) return false;
		return true;
	}

	/// <summary>Apply a vibe string onto <paramref name="c"/> in place. Returns false (touching
	/// nothing) if it is not a vibe — callers that need to TELL the listener use
	/// <see cref="SeedCodec.TryParse"/>, which is where the message lives.</summary>
	public static bool Apply( string vibe, MusicGen.Config c )
	{
		if ( c == null || !IsVibe( vibe ) ) return false;
		vibe = vibe.ToLowerInvariant();
		for ( int v = 0; v < Voices.Length; v++ )
			foreach ( var cell in Voices[v].Cells )
			{
				if ( cell == null ) continue;
				int q = Hex.IndexOf( vibe[Pos( v, cell.Column )] );
				cell.SetNorm( c, q / (float)(Levels - 1) );
			}
		return true;
	}

	// ── Rolling ───────────────────────────────────────────────────────────────────────────

	/// <summary>
	/// Roll a whole vibe STRING — every cell of the global grid, genre-independent.
	///
	/// This is the one definition of what "reroll" means, shared by every player, so the two
	/// drivers cannot answer the question differently. It produces a string rather than editing a
	/// Config on purpose: a rolled vibe is full width like any other, so it can be pinned into a
	/// seed and heard identically under a genre that was rolled separately.
	///
	/// Randomness is the CALLER's: <paramref name="rnd"/> returns values in [0,1). A driver that
	/// wants a throwaway roll passes a session RNG; one that wants a reproducible roll passes a
	/// seeded stream (see <see cref="SeedCodec.RollVibeFor"/>). The engine stays free of any
	/// ambient RNG.
	/// </summary>
	public static string RollVibe( Func<float> rnd )
	{
		if ( rnd == null ) return new string( Hex[0], VibeLength );
		var sb = new StringBuilder( VibeLength );
		for ( int i = 0; i < VibeLength; i++ )
		{
			if ( !HasCell( i ) ) { sb.Append( Hex[0] ); continue; }   // a hole stays a hole
			// Guard the top of the range: a generator returning exactly 1.0 must not index off the
			// end of the alphabet.
			int q = (int)(rnd() * Levels);
			sb.Append( Hex[Math.Clamp( q, 0, Levels - 1 )] );
		}
		return sb.ToString();
	}

	/// <summary>Roll a genre index from the same kind of caller-owned stream.</summary>
	public static int RollGenre( Func<float> rnd )
		=> rnd == null ? 0 : Math.Clamp( (int)(rnd() * GenreCount), 0, GenreCount - 1 );

	/// <summary>Roll the per-instrument volumes of <paramref name="genre"/> in place — the one
	/// thing a vibe string cannot carry, for a driver that wants the mix rolled too.</summary>
	public static void RollVolumes( int genre, MusicGen.Config c, Func<float> rnd )
	{
		if ( c == null || rnd == null ) return;
		foreach ( var f in Fields( genre ) )
			if ( IsVolume( f ) ) f.SetNorm( c, rnd() );
	}
}
gamah.skafinity / .obj/__compiler_extra.cs
Game library
global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "Skafinity" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "skafinity" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "gamah" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "gamah.skafinity" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "28" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]

[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "2026-08-13T10:27:48.6244157Z" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.110.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.110.0")]
gamah.skafinity / Engine/Melody.cs
Game library
using System;
using System.Collections.Generic;

namespace Skafinity;

/// <summary>
/// The TUNE — the part of a song a listener could hum back.
///
/// Everything the engine generated before this was accompaniment plus an improvisation: the
/// chordal voices played a rhythm figure, and the lead invented a fresh phrase every two bars.
/// That is a backing track, not a song. Real rock, punk, ska and pop songs are built on a
/// MELODY that recurs — the chorus states the same tune every time it comes round, and that
/// repetition is what makes it a chorus rather than another eight bars.
///
/// A tune is a <see cref="Pattern"/> whose cell values are SCALE DEGREES relative to the key's
/// tonic (not to the current chord), so the line keeps its shape while the harmony moves under
/// it — which is what a melody is. <see cref="MusicGen.RenderTune"/> resolves a degree against
/// the bar's chord on the strong beats, so the tune stays consonant without being re-written
/// chord by chord.
///
/// Because it is a Pattern it inherits everything patterns get: it anchors to the section (so a
/// four-bar tune restarts with the chorus) and it stretches under a half-time feel.
/// </summary>
static class Melody
{
	/// <summary>Cell value for a rest — no onset, the previous note holds.</summary>
	public const int Rest = Harmony.Rest;

	/// <summary>The AMBITUS — the range a whole tune is written in, in SCALE DEGREES from the key's
	/// tonic: from the sixth below it up to the third above the octave. Twelve degrees, about an
	/// octave and a fifth in a major scale, and deliberately lopsided — a melody sits above its
	/// tonic and only dips under it, so a symmetric range would spend half of itself where no tune
	/// goes.
	///
	/// It is an authored bound rather than a measured one: what it is FOR is that a line which
	/// wanders further stops being singable, and singable is what makes the thing a tune. The
	/// number is a judgement about that and nothing more.</summary>
	public const int DegreeMin = -2, DegreeMax = 9;

	/// <summary>How many scale degrees ONE PHRASE may cover — eight, an octave in a major scale,
	/// inside the twelve the whole tune may reach.
	///
	/// A RANGE AND AN AMBITUS ARE TWO DIFFERENT NUMBERS AND ONE CANNOT DO BOTH JOBS. The ambitus is
	/// a whole-song figure — how far the tune goes over all of it — and a tune here is 2–8 bars, so
	/// bounding a single phrase with it was measuring one thing and spending it on another. What a
	/// phrase actually does is orbit a register: it opens somewhere, moves about an octave around
	/// that, and the tune gets its wider reach from the phrases sitting in DIFFERENT places rather
	/// than from any one of them wandering.
	///
	/// So the window is drawn per phrase and anchored on the note the phrase opens on
	/// (<see cref="Opens"/>), which is why the opening degree keeps its weighting instead of being
	/// folded into a window drawn first. Inside a phrase this is the bound the walk reflects off
	/// and the centre <see cref="Centre"/> pulls toward; the ambitus stays the outer wall.
	///
	/// THE WINDOW BOUNDS WHERE A LINE WANDERS, NOT WHERE IT MAY BE PUT. An answer transposes its
	/// call bodily (<see cref="AnswerOp.SequenceUp2"/> takes it up two degrees), and that is a
	/// deliberate move rather than a walk drifting out of register — so <see cref="Answer"/>
	/// reflects off the ambitus. Folding a sequence back into the call's window would flatten the
	/// one gesture in the tune whose whole point is that it goes somewhere else.
	///
	/// Authored like everything else here (there is no melodic corpus in this repo). The published
	/// pop-melody work that gives whole-song ambitus at around two octaves measures range on a
	/// rolling two-bar window for exactly this reason, but its figures are for a different roster
	/// and are not borrowed as a number — this is a judgement about a phrase being one gesture in
	/// one register, and <c>--stats</c> reports what the engine actually does with it.</summary>
	public const int PhraseSpan = 8;

	/// <summary>The note lengths a tune may be written in, in ticks: sixteenth, eighth, dotted
	/// eighth, quarter, dotted quarter, half. <see cref="Timing.TicksPerBeat"/> is 48, so every one
	/// of them is exact and <see cref="Timing"/> needs nothing — the same clean division the
	/// thirty-second work already proved.
	///
	/// The line every genre's weights are split on is the QUARTER: the first three are shorter than
	/// a beat and the last three are a beat or longer, which is what <c>move</c> leans on to make a
	/// verse sparser than its chorus without a second density mechanism.</summary>
	public static readonly int[] Lengths = { 12, 24, 36, 48, 72, 96 };

	/// <summary>Index of the first length that is a beat or longer.</summary>
	const int LongFrom = 3;

	/// <summary>How a phrase answers itself. The answer used to not be DRAWN at all: it was the
	/// call's degrees minus one, every genre, every song, with a forced tonic on the end — so half
	/// of every tune in the engine was a mechanical transform of the other half.</summary>
	public enum AnswerOp
	{
		/// <summary>The call a step lower — the old behaviour, and still the heaviest weight in
		/// most genres because it is genuinely the commonest answer in this music.</summary>
		Transpose,
		/// <summary>The same line with a different landing: identical degrees, and the last two
		/// re-drawn to step home. What a chorus does.</summary>
		NewTail,
		/// <summary>The call restated a degree higher — a question answered with a bigger question,
		/// which still resolves because the last note is the tonic either way.</summary>
		SequenceUp,
		/// <summary>Restated two degrees higher.</summary>
		SequenceUp2,
		/// <summary>Mirrored about the call's first degree: where the call rose, the answer falls.
		/// </summary>
		Invert,
	}

	public static readonly AnswerOp[] Answers =
	{
		AnswerOp.Transpose, AnswerOp.NewTail, AnswerOp.SequenceUp, AnswerOp.SequenceUp2, AnswerOp.Invert,
	};

	/// <summary>How the CONSEQUENT opens — the one decision that makes a period a period.
	///
	/// A period is two call/answer pairs: an antecedent that leaves the line open and a consequent
	/// that closes it. Both pairs are built by exactly the machinery below; what varies is how much
	/// of the antecedent's call the consequent's call keeps. That is the classical taxonomy and it
	/// is also the whole variation budget — the answers repeat their own call's rhythm either way,
	/// so if the consequent's call does not move, nothing in the tune's second half is new.</summary>
	public enum PeriodShape
	{
		/// <summary>PARALLEL — the consequent restates the call note for note and differs only in
		/// how it answers. The commonest period in this music, and the one that makes the tune
		/// unmistakably one tune; it is also the least new material, which is why it is not the
		/// only shape.</summary>
		Parallel,
		/// <summary>VARIED — the consequent keeps the call's rhythm and sings a fresh contour over
		/// it. The rhythm is what a listener remembers, so this reads as the same phrase said again
		/// differently rather than as a second idea.</summary>
		Varied,
		/// <summary>CONTRASTING — the consequent opens with a phrase of its own, rhythm and all.
		/// The departure, and the only shape that puts a second rhythm in the tune.</summary>
		Contrasting,
	}

	public static readonly PeriodShape[] Shapes =
	{
		PeriodShape.Parallel, PeriodShape.Varied, PeriodShape.Contrasting,
	};

	/// <summary>Authored, not measured — there is no melodic corpus in this repo (see the note on
	/// <c>GenreProfile.Tune</c>) and this is a judgement about how much a tune may move under
	/// itself. It is one table rather than six because nothing found says a genre has an opinion
	/// about it; a genre that turns out to want one puts weights in <see cref="TuneVocab"/>, the
	/// way <see cref="Answers"/> already does.</summary>
	static readonly int[] ShapeWeights = { 4, 3, 3 };

	/// <summary>Where an ANTECEDENT lands: a chord tone that is not the tonic, which is what leaves
	/// the line open. The fifth is the half cadence proper and takes most of the weight; the third
	/// is the softer one. Landing home here would close the tune half way through it and make the
	/// consequent an appendix rather than an answer.</summary>
	static readonly int[] HalfCadence = { 4, 2 };
	static readonly int[] HalfCadenceWeights = { 3, 2 };

	/// <summary>The fewest bars a phrase may be. A period is four phrases, so a tune shorter than
	/// four of these is two phrases and no period — a one-bar "phrase" is a fragment, and four of
	/// them is a tune that restates itself every bar, which is the defect this exists to fix
	/// arriving from the other direction.</summary>
	public const int MinPhraseBars = 2;

	/// <summary>How many phrases a tune of <paramref name="bars"/> bars is written in: four (a
	/// period) where they are long enough to be phrases, two (a plain call and answer) otherwise.
	/// </summary>
	public static int PhraseCount( int bars ) => bars >= 4 * MinPhraseBars ? 4 : 2;

	/// <summary>Where a tune may open — chord tones only, weighted toward the tonic and the fifth,
	/// with the octave reachable.</summary>
	static readonly int[] Opens = { 0, 2, 4, 7 };
	static readonly int[] OpenWeights = { 5, 3, 4, 2 };

	/// <summary>How far the phrase leans uphill at its start and downhill at its end — the MELODIC
	/// ARCH. Phrases in this music (and in every corpus anyone has counted) rise and then fall on
	/// average, and a plain random walk does not: it wanders, and the only thing that ever brought
	/// it home was the forced tonic on the last note, which is a landing with no approach to it.
	///
	/// 0 would be the old coin toss; 0.5 would make direction deterministic and turn every tune
	/// into the same hill. This is a lean on a draw, not a shape imposed on one.</summary>
	const float Arch = 0.25f;

	/// <summary>How hard the line is pulled back toward the middle of its PHRASE WINDOW —
	/// TESSITURA, the fact that a melody orbits a central pitch rather than diffusing across
	/// everything it is allowed to sing.
	///
	/// It is what actually keeps a tune off the range ends. <see cref="Reflect"/> is a BACKSTOP: it
	/// stops a line parking at a boundary, but a walk with no centre still spends its time out
	/// there, and the arch makes that worse in the first half of every phrase by leaning uphill
	/// whatever the register already is. The two are different jobs and both are needed — this
	/// decides where the line lives, reflection decides what happens when it arrives at an edge
	/// anyway. The centre it pulls toward is the PHRASE's (<see cref="PhraseSpan"/>), so a phrase
	/// orbits its own register rather than the middle of everything the tune may reach.</summary>
	const float Centre = 0.30f;

	/// <summary>
	/// Draw a tune: <paramref name="bars"/> bars built as a PERIOD where they are long enough for
	/// one, and as a plain call and answer where they are not.
	///
	/// A call and answer is a pair of phrases — the first states a shape and leaves it open, the
	/// second repeats that rhythm and resolves it home. That symmetry is most of what makes a line
	/// sound composed rather than generated, and a fresh random phrase every two bars never sounds
	/// like a tune however good the notes are.
	///
	/// A PERIOD IS TWO OF THOSE PAIRS AND SITS ABOVE THEM, NOT INSTEAD OF THEM. The antecedent
	/// (call, answer) lands on a chord tone that is not the tonic and so leaves the line open; the
	/// consequent (call, answer) opens from the antecedent — restating it, varying it, or departing
	/// from it (<see cref="PeriodShape"/>) — and resolves home. That is what puts repetition at the
	/// whole tune's length and variation at a phrase's, instead of the binary shape the tune had
	/// before this: two phrases, one rhythm between them, looped to fill the section and repeated
	/// identically at every chorus.
	///
	/// THE RHYTHM REPEAT STAYS, WITHIN A PAIR. Varying an answer's rhythm stops its two phrases
	/// being heard as a question and an answer at all; the only rhythmic freedom an answer gets is
	/// where its last notes land, and that arrives through <see cref="AnswerOp.NewTail"/> rather
	/// than through a second rhythm draw. A new rhythm enters a tune at the CONSEQUENT'S CALL or
	/// nowhere. The 100%-tonic ending stays too — that is not a defect to be varied away, it is
	/// what makes the thing a tune.
	/// </summary>
	/// <param name="v">The genre's vocabulary — the note lengths it sings in, how often it rests,
	/// how often it leaps, and how it answers itself.</param>
	/// <param name="move">How much this line moves relative to the genre's own table: 1 for a
	/// chorus, less for the sparser verse tune. It leans the length draw toward the long end rather
	/// than being a second density knob sitting beside the weights.</param>
	/// <param name="swung">True where the song swings or shuffles. THE SIXTEENTH COMES OUT OF THE
	/// MENU: under a shuffle the beat's own subdivision IS the triplet, and Timing's warp puts a
	/// straight sixteenth at a third of the beat while the band's eighth-based figures sit on the
	/// beat and at two thirds. That is not syncopation, it is two grids at once, and it reads as
	/// the lead pushing against a band it does not line up with. A shuffled genre's melody moves in
	/// eighths and the shuffle does the subdividing.</param>
	public static Pattern Draw( Rng rng, int bars, int barTicks, in TuneVocab v, float move = 1f,
		bool swung = false )
	{
		int phrases = PhraseCount( bars );
		int phraseTicks = barTicks * Math.Max( 1, bars / phrases );
		var ticks = new List<int>();
		var degrees = new List<int>();

		// The genre's length table, leaned toward the long end for a verse. At move = 1 both
		// factors are 1 and the table is the genre's verbatim.
		var weights = new int[Lengths.Length];
		for ( int i = 0; i < weights.Length; i++ )
		{
			float w = v.LengthWeights[i] * (i < LongFrom ? move : 2f - move);
			// Anything that does not divide the eighth: the sixteenth AND the dotted eighth, which
			// lands mid-eighth for the same reason and was the half of this that was easy to miss.
			if ( swung && Lengths[i] % Timing.TicksPerEighth != 0 ) w = 0f;
			weights[i] = Math.Max( 0, (int)MathF.Round( w * 8f ) );
		}

		// ── the antecedent ──
		var callRhythm = DrawRhythm( rng, phraseTicks, weights, v );
		var callDegrees = DrawContour( rng, callRhythm.Count, v );
		Emit( ticks, degrees, 0, callRhythm, callDegrees );

		// A HALF CADENCE IS WHAT MAKES THE CONSEQUENT NECESSARY. With a period the antecedent lands
		// on a chord tone that is not the tonic and stays open; with only two phrases there is
		// nothing after it, so it resolves the way it always did.
		int open = phrases == 4 ? HalfCadence[rng.WeightedIndex( HalfCadenceWeights )] : 0;
		Emit( ticks, degrees, phraseTicks, callRhythm, Answer( rng, callDegrees, v, open ) );

		if ( phrases == 4 )
		{
			var shape = rng.PickWeighted( Shapes, ShapeWeights );
			// BOTH DRAWN FOR EVERY SHAPE, so swapping one shape for another does not shift the rest
			// of the tune's stream — the discipline PickOrNull keeps in the composer. A parallel
			// consequent pays for a phrase it does not sing.
			var freshRhythm = DrawRhythm( rng, phraseTicks, weights, v );
			var conRhythm = shape == PeriodShape.Contrasting ? freshRhythm : callRhythm;
			var freshDegrees = DrawContour( rng, conRhythm.Count, v );
			var conDegrees = shape == PeriodShape.Parallel ? callDegrees : freshDegrees;

			Emit( ticks, degrees, 2 * phraseTicks, conRhythm, conDegrees );
			// The consequent answers with its own operator — that is what a parallel period varies,
			// and it is the only thing it varies.
			Emit( ticks, degrees, 3 * phraseTicks, conRhythm, Answer( rng, conDegrees, v, 0 ) );
		}

		// A held final note, so the tune breathes before it comes round again.
		return new Pattern( bars * barTicks, ticks.ToArray(), degrees.ToArray() );
	}

	/// <summary>Append one phrase's onsets (offset to <paramref name="at"/>) and its degrees.
	/// </summary>
	static void Emit( List<int> ticks, List<int> degrees, int at, List<int> rhythm, List<int> pitches )
	{
		for ( int i = 0; i < rhythm.Count; i++ ) { ticks.Add( at + rhythm[i] ); degrees.Add( pitches[i] ); }
	}

	/// <summary>One phrase's RHYTHM — the onsets, in ticks from the phrase's own start.
	///
	/// Rhythm first, and separately from the pitches: a melody's rhythm is what gets remembered,
	/// and drawing it on its own is what lets an answer repeat it exactly.
	///
	/// A REST IS AN OMITTED ONSET, not a cell. Leaving the tick out means the previous note's
	/// SpanTicks simply grows to cover the gap, and RenderTune's two-beat length cap turns the
	/// remainder into real silence — so rests cost the renderer nothing. A Melody.Rest CELL
	/// would be read as a DEGREE by RenderTune, which has no rest arm, and sung.</summary>
	static List<int> DrawRhythm( Rng rng, int phraseTicks, int[] weights, in TuneVocab v )
	{
		var rhythm = new List<int>();
		for ( int t = 0; t < phraseTicks; )
		{
			// THE PHRASE RE-ANCHORS TO THE BEAT, and without this the widened vocabulary is worse
			// than the two lengths it replaced. Free-running the accumulator over the menu means a
			// dotted eighth or a sixteenth shifts EVERY REMAINING NOTE of the phrase by a non-beat
			// amount, permanently — the line rotates against the bar and never comes back, which is
			// a 3-against-4 running for eight bars rather than a melody. It reads as the lead being
			// out of time with the band, because it is.
			//
			// The rule is the one a player reads off a stave: inside a beat you may only play what
			// fits the rest of it. So a dotted eighth is followed by a sixteenth, a sixteenth by
			// whatever fills the remaining three, and the next beat starts on the beat. Notes still
			// land on the "and" and on sixteenths — what they cannot do is drift.
			int inBeat = t % Timing.TicksPerBeat;
			int len;
			if ( inBeat == 0 ) len = Lengths[rng.WeightedIndex( weights )];
			else
			{
				int room = Timing.TicksPerBeat - inBeat;
				var fits = new int[Lengths.Length];
				bool any = false;
				for ( int i = 0; i < Lengths.Length; i++ )
					if ( Lengths[i] <= room ) { fits[i] = weights[i]; any |= weights[i] > 0; }
				len = any ? Lengths[rng.WeightedIndex( fits )] : room;
			}
			// Never open the phrase on silence: a tune that starts by not being there has no shape
			// for the answer to repeat.
			if ( rhythm.Count == 0 || !rng.Chance( v.Rest ) ) rhythm.Add( t );
			t += len;
		}
		// A call of one note is not a call. Only reachable when every cell after the first drew a
		// rest, which is rare and still worth not shipping.
		if ( rhythm.Count < 2 ) rhythm.Add( phraseTicks / 2 );
		return rhythm;
	}

	/// <summary>One phrase's CONTOUR — <paramref name="notes"/> degrees relative to the key's tonic.
	///
	/// Three things shape it and none of them is a random walk: the arch (see <see cref="Arch"/>),
	/// post-skip reversal (below), and reflection off the range ends instead of a clamp.
	///
	/// A phrase opens on a CHORD TONE — a melody that opens on the second or the seventh is a
	/// melody that starts by needing to resolve — weighted toward the tonic and the fifth, where
	/// far more tunes actually start, with the octave reachable. A uniform draw over three values
	/// is the sort of thing that shows up in a sweep as 33/33/33 and in a listen as "they all start
	/// the same way".</summary>
	static List<int> DrawContour( Rng rng, int notes, in TuneVocab v )
	{
		var degrees = new List<int>( notes );
		int degree = Opens[rng.WeightedIndex( OpenWeights )];
		// THE PHRASE'S OWN WINDOW, drawn around the note the phrase opens on so that the opening
		// degree keeps its weighting and cannot land outside its own register. Where the window may
		// sit is what gives the tune its wider reach: two phrases an octave apart cover the ambitus
		// between them without either of them wandering.
		int loMin = Math.Max( DegreeMin, degree - (PhraseSpan - 1) );
		int loMax = Math.Min( degree, DegreeMax - (PhraseSpan - 1) );
		if ( loMax < loMin ) loMax = loMin;
		int lo = Math.Min( loMin + rng.Int( loMax - loMin + 1 ), DegreeMax );
		int hi = Math.Min( lo + PhraseSpan - 1, DegreeMax );
		int owed = 0;
		for ( int i = 0; i < notes; i++ )
		{
			degrees.Add( degree );

			bool leap = rng.Next() < v.Leap;
			// A leap is a third, a fourth or a fifth. It used to be a third and nothing else, in
			// every genre and every song — "a leap" was one interval wearing a general name.
			int size = leap ? 2 + rng.Int( 3 ) : 1;
			int sign;
			if ( owed != 0 )
			{
				// POST-SKIP REVERSAL: a melody that jumps comes back. It is one of the most robust
				// findings there is about how tunes are actually written, and it is also what makes
				// a leap read as a gesture rather than as the line relocating.
				sign = owed;
				owed = 0;
			}
			else
			{
				float u = notes < 2 ? 0.5f : i / (float)(notes - 1);
				// Where in the PHRASE'S window this note sits, −1 at the bottom and +1 at the top.
				float mid = (lo + hi) / 2f, half = Math.Max( 1f, (hi - lo) / 2f );
				float pos = (degree - mid) / half;
				sign = rng.Chance( Math.Clamp( 0.5f + Arch * (1f - 2f * u) - Centre * pos, 0.05f, 0.95f ) )
					? 1 : -1;
			}
			if ( leap ) owed = -sign;
			degree = Reflect( degree + sign * size, lo, hi );
		}
		return degrees;
	}

	/// <summary>ANSWER a call: the same rhythm, the call's degrees put through one of the genre's
	/// <see cref="AnswerOp"/>s, landing on <paramref name="last"/> — the tonic where this phrase
	/// closes the tune, an open chord tone where it is an antecedent handing over to a consequent.
	/// Every operator answers the same question and every one of them lands in the same place.
	/// </summary>
	static List<int> Answer( Rng rng, List<int> call, in TuneVocab v, int last )
	{
		var op = rng.PickWeighted( Answers, v.AnswerWeights );
		// Drawn for every operator, so swapping one for another does not shift the rest of the
		// tune's stream — the same discipline PickOrNull keeps in the composer.
		int approach = rng.Chance( 0.65f ) ? 1 : -1;
		int n = call.Count;
		int first = call[0];
		var answer = new List<int>( n );
		for ( int i = 0; i < n; i++ )
		{
			if ( i == n - 1 ) { answer.Add( Reflect( last ) ); continue; }
			int d = op switch
			{
				AnswerOp.NewTail => i == n - 2 ? last + approach : call[i],
				AnswerOp.SequenceUp => call[i] + 1,
				AnswerOp.SequenceUp2 => call[i] + 2,
				AnswerOp.Invert => 2 * first - call[i],
				_ => call[i] - 1,
			};
			answer.Add( Reflect( d ) );
		}
		return answer;
	}

	/// <summary>Fold a degree back inside the singable range by REFLECTING off its ends.
	///
	/// A clamp is sticky: a line that reaches a boundary and keeps stepping outward parks there,
	/// which is where 10–18% of every tune's notes sat and where most of its repeated adjacent
	/// notes came from — the two are the same defect seen from either side. Reflection keeps the
	/// range (that is what makes a tune singable) and turns the wall into a turn.
	///
	/// Off the AMBITUS — the outer wall, which is what a transposed answer folds against.</summary>
	internal static int Reflect( int degree ) => Reflect( degree, DegreeMin, DegreeMax );

	/// <summary>Fold a degree back inside an arbitrary window by reflecting off its ends — the
	/// walk inside one phrase uses its own (<see cref="PhraseSpan"/>).</summary>
	internal static int Reflect( int degree, int lo, int hi )
	{
		for ( int guard = 0; guard < 8 && (degree < lo || degree > hi); guard++ )
		{
			if ( degree < lo ) degree = 2 * lo - degree;
			if ( degree > hi ) degree = 2 * hi - degree;
		}
		return Math.Clamp( degree, lo, hi );
	}
}

// The tune, and how a bar of it is played. Part of the MusicGen engine — see MusicGen.cs.

public sealed partial class MusicGen
{
	// The song's tunes, drawn once per song off their own streams (so having them shifts nothing
	// else in the composition) and keyed by SECTION TYPE. The chorus tune is the hook: identical
	// every chorus, which is the whole reason a chorus reads as one. The verse tune is a second,
	// sparser line — same song, different words.
	Pattern _chorusTune, _verseTune;

	// How long one PHRASE of them is. A diagnostic wanting to read a tune phrase by phrase cannot
	// re-derive this without re-deciding the period, which is the re-implementation PlanTrace
	// exists to avoid — so the composer writes it down.
	int _tunePhraseTicks;

	/// <summary>One phrase of the song's tunes, in ticks (diagnostics — see <see cref="Melody"/>).
	/// </summary>
	internal int TunePhraseTicks => _tunePhraseTicks;

	/// <summary>The tune this section sings, or null where the section is not a place for one:
	/// a solo is where the genre's lead grammar improvises, an intro is a build-in, and the
	/// ending has already resolved.</summary>
	Pattern TuneFor( Section s ) => !SectionSingsTune( s ) ? null
		: s == Section.Chorus ? _chorusTune : _verseTune;

	/// <summary>Whether a section TYPE is a place for a tune at all. Static because it is a
	/// property of the form rather than of a drawn song — which is what lets a form be checked for
	/// putting its feel changes somewhere the melody can contrast with them.</summary>
	internal static bool SectionSingsTune( Section s ) =>
		s is Section.Chorus or Section.Verse or Section.PreChorus or Section.Bridge;

	/// <summary>Draw the song's tunes — one for choruses, a sparser one for verses. Every genre
	/// gets both: "riff-led" does not mean melody-free, and metal verses with no tune left four
	/// and eight bar holes where the lead simply did not play.</summary>
	void DrawTunes( int barTicks, bool swung )
	{
		// The vocabulary is the GENRE's (GenreProfile.Tune). It used to be a switch on _prof.Lead
		// right here, which is the `if ( _genre == … )` smell one level removed: two genres sharing
		// a LeadStyle got the same tune vocabulary, and where their densities matched the draws
		// agreed and the tunes came back identical.
		// The tune is a WHOLE NUMBER OF HARMONIC CYCLES — the bars it takes the progression to come
		// round (ChordBars x the progression's length), capped at eight. A four-bar tune over an
		// eight-bar cycle states itself twice, and the second statement lands over different
		// chords than it was written against: same notes, different harmony, which is exactly the
		// "the lead clashes with the backing" it sounds like. Matching the cycle means every
		// repetition sits over the changes it was drawn for.
		int cycle = Math.Clamp( _chordBars * _prog.Length, 2, 8 );
		// A PERIOD NEEDS FOUR PHRASES, AND A WHOLE NUMBER OF CYCLES IS STILL ALIGNED TO THE CHANGES.
		// The clamp above is not about length, it is about a tune's statements landing over the
		// chords they were drawn against — and a tune of exactly two cycles does, bar for bar. So a
		// genre whose cycle is short doubles the tune rather than being stuck with two phrases: punk
		// and pop (ChordBars 1 x a four-chord progression) went from a 4-bar tune whose rhythmic cell
		// was 2 bars, stated twice and looped, to an 8-bar period. Eight is the ceiling because a
		// section is eight bars — a tune longer than the section it is sung in never finishes.
		int bars = cycle;
		while ( bars * 2 <= 8 ) bars *= 2;
		_tunePhraseTicks = barTicks * bars / Melody.PhraseCount( bars );
		// THE GENRE IS IN THE TUNE'S STREAM, and it is the only stream it is in.
		//
		// Without it the genre reached Draw through nothing but `density` and `leap`, so where two
		// genres' densities were close the draws mostly agreed and the tunes came back
		// BYTE-IDENTICAL: over 500 songs, rock and country sang the same melody 53% of the time and
		// punk and pop 52%. Same n, different key, different kit, literally the same tune — which is
		// most of why the roster read as one band.
		//
		// The SONG stream (ComposePlan's `new Rng( _tag )`) still has no genre in it, and that is a
		// feature rather than an oversight: genre 0 and genre 3 at the same tag:n share the root
		// note, the pan, the ride preference and the whole kit draw, so the same song in two genres
		// is a thing the toy can do. That is worth more than the variation putting the genre there
		// would buy, and it is why the genre goes in the TUNE streams and nowhere else.
		//
		// IT IS A DIFFERENT DRAW, NOT A GUARANTEED DIFFERENT TUNE, and that distinction is the
		// point. Two genres landing on a similar melody at one seed is the toy doing what it is
		// for; what was wrong before was that they landed there RELIABLY, off a stream that could
		// not tell them apart. Nothing here should ever grow into machinery that forces two genres
		// to diverge — the collision rate is something `--stats` reports, not something the engine
		// enforces.
		_chorusTune = Melody.Draw( new Rng( $"{_tag}:tune:{_genre}:chorus" ), bars, barTicks, _prof.Tune, 1f, swung );
		// The verse tune is the same vocabulary, sung with fewer notes in it — same song,
		// different words.
		_verseTune = Melody.Draw( new Rng( $"{_tag}:tune:{_genre}:verse" ), bars, barTicks, _prof.Tune, 0.8f, swung );
	}

	/// <summary>Play one bar of the section's tune.
	///
	/// Degrees are relative to the KEY, so the tune keeps its shape as the chords move. What
	/// keeps it consonant is resolution on the strong beats only: a note landing on a beat is
	/// pulled to the nearest tone of the bar's chord, while the notes between beats are free to
	/// pass through. Snapping everything would rewrite the tune chord by chord — which is
	/// exactly the "no tune, just an improvisation over the changes" this replaces.</summary>
	void RenderTune( Pattern tune, int barTick, int barTicks, int chord, Rng rng, Rng exprRng )
	{
		int melBase = LeadBase();
		var tones = ChordDegrees( chord );
		bool guitarLead = !_hornLead;
		float amp = (guitarLead ? _c.LeadGtrVol * _c.LeadGtrBalance : _c.MelodyVol * _c.MelodyBalance)
			* _midMul;
		float drive = guitarLead ? _c.LeadGtrDrive : _c.MelodyDrive;
		var ex = guitarLead ? Expr( "LEAD GTR" ) : Expr( "LEAD" );
		int prevMidi = NoPrev;

		// A SECTION SHORTER THAN THE TUNE SINGS THE TUNE'S END, not its beginning. A four-bar
		// pre-chorus over an eight-bar tune stated the call and was cut off by the chorus before
		// the answer ever arrived — a phrase interrupted by the next phrase, which is what "two
		// ideas at once" sounds like. Pulling the anchor back lands the tune's resolution exactly
		// on the section's last bar, which is what a pre-chorus is for.
		int anchor = _sectionTicks > 0 && _sectionTicks < tune.LengthTicks
			? _sectionTick - (tune.LengthTicks - _sectionTicks)
			: _sectionTick;
		// THE TUNE IS EXEMPT FROM THE SECTION'S FEEL, and that exemption IS half/double time.
		// Part.Feel is the RHYTHM SECTION's pattern rate: when a section halves or doubles, the
		// band changes rate underneath a vocal that stays exactly where it was — that contrast is
		// the entire gesture, and it is what makes a double-time chorus lift rather than sound
		// like the tape sped up. Scaling the hook by the same multiplier deletes the gesture and
		// leaves only a faster song. So the tune slices at the nominal rate; every other voice
		// (comp, keys, bass, horns, kit) reads _feel.
		var sung = tune.Slice( barTick, barTick + barTicks, anchor );
		Trace?.Add( TraceVoice.Tune, sung );
		foreach ( var h in sung )
		{
			int degree = h.Value;
			int len = Math.Min( h.SpanTicks, Timing.TicksPerBeat * 2 );
			bool onBeat = (h.Tick - _barTick) % Timing.TicksPerBeat == 0;

			// What resolves is the note the ear has TIME to hear against the chord: anything on a
			// beat, and anything held for a beat or more. A quick note between beats is a passing
			// tone and is left alone — that is the difference between a melody and an arpeggio.
			// (Snapping only the on-beat notes left long off-beat non-chord tones ringing over the
			// backing for up to two beats, which is what a clash sounds like.)
			bool resolve = onBeat || len >= Timing.TicksPerBeat;
			if ( resolve ) degree = NearestChordTone( tones, degree );
			int midi = ScaleMidi( melBase, degree );
			// The degree snap chose WHICH chord tone; this puts the note on the pitch the chord
			// actually sounds, which is not the same thing on every degree (see NearestSoundingTone).
			if ( resolve ) midi = NearestSoundingTone( midi, chord, h.Tick );
			// Where this note sits in the TUNE, which is the phrase a bend leans into. The tune's
			// own length is the cycle, so this is the same 0..1 whatever bar the section is on.
			float pu = ((h.Tick - anchor) % tune.LengthTicks + tune.LengthTicks)
					% tune.LengthTicks / (float)tune.LengthTicks;
			var vc = Roll( ex, midi, prevMidi, exprRng, (float)_time.SpanSeconds( h.Tick, len ),
					BendBias( len, pu ) );
			prevMidi = midi;
			RenderLeadNote( _time.TickToSample( h.Tick ), _time.SpanSamples( h.Tick, len * 0.92 ),
				midi, amp * NoteGain( h.Vel ), _time.SpanSeconds( h.Tick, len ) * 0.8,
				drive, vc );

			// The genre's own hand on the same tune: country punctuates it with double-stops, metal
			// runs between its notes. The line is the same either way — this is ORNAMENT, not a
			// different melody, which is the difference between a genre playing a song and a genre
			// having its own song. Ornament also means occasional: harmonising every long note in
			// parallel thirds replaces the melody with a two-note chord (see EmitDoubleStop).
			if ( _prof.Lead == LeadStyle.DoubleStop && len >= Timing.TicksPerEighth * 2
				&& rng.Chance( DoubleStopChance ) )
				EmitDoubleStop( h.Tick, len, degree, amp * NoteGain( h.Vel ) );
			else if ( _prof.Lead == LeadStyle.Shred && len >= Timing.TicksPerBeat && rng.Chance( 0.18f ) )
				for ( int k = 1; k <= 3; k++ )
				{
					int m2 = ScaleMidi( melBase, degree + k );
					RenderLeadNote( _time.EvenSpan( h.Tick + len / 2, len / 2, (k - 1) / 3.0 ),
						_time.SpanSamples( h.Tick, len / 8.0 ), m2, amp * 0.8f * NoteGain( h.Vel ),
						_time.SpanSeconds( h.Tick, len / 8.0 ) * 0.8, drive, vc );
				}
		}
	}
}
gamah.skafinity / Engine/Synth/Patch.cs
Game library
using System;
using System.Collections.Generic;

namespace Skafinity;

// Patch — the subtractive voice definition every pitched note is rendered through:
// unison oscillators → optional high-pass → resonant low-pass with a cutoff envelope.
//
// Part of the MusicGen engine — see MusicGen.cs.

// ── Synth core: unison osc → optional high-pass → resonant low-pass (cutoff
//    envelope) → soft drive → AD/sustain amp env. ──
struct Patch
{
	public int Osc;        // 0 sine 1 saw 2 square 3 triangle
	public int Voices;
	public float Detune;   // cents
	public float Amp;
	public float Attack;   // sec
	public double Decay;   // sec (exp time constant)
	public float Sustain;  // 0..1 (only if Sustained)
	public bool Sustained;
	public float Cutoff;   // Hz low-pass
	public float CutEnv;   // Hz added at attack, decays with Decay
	public float Reso;     // SVF damping (lower = more resonance)
	public float Highpass; // Hz one-pole high-pass (0 = off)
	public float Drive;    // tanh
	public float Pan;      // -1..1
	public float Vibrato;  // Hz (rate of the pitch wobble)
	public float Breath;   // 0..1 noise mix (reeds)
	// ── Expression (per-note pitch shaping; see Expression/Voicing) ──
	public float VibDepth;   // vibrato depth as a pitch fraction (0 → legacy 0.005 when Vibrato>0)
	public float BendSemis;  // pitch offset in semitones at note START, glides to 0 (bend-in / glide); −ve starts below
	public float BendTime;   // 0..1 fraction of the note over which BendSemis glides to 0
	public float ScoopSemis; // height (semitones) of a mid-note bend-up-and-back hump (0 = none)
	// THE BEND — the one a listener would name as one, and the only gesture here that moves the
	// note AWAY from its pitch rather than easing onto it. Everything above is an approach: it
	// starts off-pitch and resolves. This starts ON pitch, pushes up by BendUpSemis, and stays
	// there — or comes back, if BendUpHold says how long to sit at the top first. All three times
	// are SECONDS, never a fraction of the note, so a bend is the same physical gesture whatever
	// the tempo and whatever the note length.
	public float BendUpSemis; // semitones bent UP part way through the note (0 = none)
	public float BendUpStart; // seconds into the note where the bend begins
	public float BendUpTime;  // seconds the bend takes to reach pitch (and to come back down)
	public float BendUpHold;  // seconds held at pitch before releasing; 0 = held to the end
	public float PhaseSeed;  // oscillator start phase (0..1); 0 = legacy in-phase start. Used to
	                         // decorrelate the two double-tracking takes (see RenderPatch).
}

public sealed partial class MusicGen
{
}
gamah.skafinity / Engine/Voices/Comp.cs
Game library
using System;
using System.Collections.Generic;

namespace Skafinity;

// The chordal layer's dispatch: the genre's comp figure, played the genre's way.
//
// Part of the MusicGen engine — see MusicGen.cs.

public sealed partial class MusicGen
{
	/// <summary>The main chordal voice for a bar. The FIGURE comes from the genre's table (a
	/// pattern with its own length, so a two-bar riff is a two-bar riff) and the STYLE says what
	/// the voice does with each hit. Three genres sharing one comping rhythm was the loudest
	/// duplication left in the engine — this is the seam that fixes it.</summary>
	/// <param name="loud">The section is loud enough that the genre changes technique — see
	/// <see cref="GenreProfile.LoudComp"/>. One voice, one chord, a different instrument gesture;
	/// the caller has already picked the matching figure.</param>
	void RenderCompVoice( int barTick, int to, int chord, Pattern fig, Rng rng, Rng exprRng,
		bool loud = false )
	{
		_compTrim = DensityTrim( fig, loud ? _prof.LoudCompFigures : _prof.CompFigures );
		var hits = fig.Slice( barTick, to, _sectionTick, _feel );
		// Remember what the riff played: where the bass doubles it (metal, and punk's unison
		// option) it reads these onsets rather than a table of its own.
		_riffOnsets.AddRange( hits );
		Trace?.Add( TraceVoice.Comp, hits );

		switch ( loud ? _prof.LoudComp : _prof.Comp )
		{
			case CompStyle.Riff: RenderRiffBar( hits, chord, rng, exprRng ); break;
			case CompStyle.BoomChick: RenderStrumBar( hits, chord, rng, exprRng ); break;
			case CompStyle.Downstroke: RenderDownstrokeBar( hits, chord, rng, exprRng ); break;
			case CompStyle.Gallop: RenderGallopBar( hits, chord, rng, exprRng ); break;
			case CompStyle.Pad: RenderPadBar( hits, chord, rng, exprRng ); break;
			default: RenderSkankBar( hits, chord, rng, exprRng ); break;
		}
	}

	/// <summary>
	/// How far the drawn comp figure is trimmed for its own DENSITY, relative to the genre's table.
	///
	/// A section draws its figure from the genre's table and the figures are not equally dense, so
	/// how loud the backing sits was decided by a draw: rock's rhythm guitar measured 4 dB apart
	/// between the seed the suite balances on and the genre's own average. Nothing was watching it —
	/// <c>--levels</c> averages the spread away and the suite's balance check is a single seed —
	/// and 4 dB is a larger move than any of the balances it sits under, handed out at random.
	///
	/// The trim is EXACTLY the arithmetic part of that and no more. N onsets of an unrelated-phase
	/// voice sum incoherently, so the level of a figure grows as √N: the trim is therefore
	/// √(table mean ÷ this figure), which cancels that growth and nothing else. What survives is
	/// everything MUSICAL about the difference — the cells' own velocities, the genre's accent
	/// weights, the section's energy, and the plain fact that a busier bar has more attacks in it.
	/// A full compensation (exponent 1) would flatten the busy figure into the sparse one, which is
	/// the mistake the other repair — levelling the figures against each other — makes by design.
	///
	/// AND IT IS MEAN-PRESERVING, which is the difference between narrowing a spread and moving a
	/// balance. √(mean ÷ d) is convex in d, so averaged over a genre's table it comes out above 1
	/// and every genre's comp would drift a little louder — a mix change smuggled in with a spread
	/// fix, and the `*Balance` values are measured numbers that would then be wrong. Dividing by the
	/// table's own average trim leaves the genre exactly where `--levels` measured it and moves only
	/// the seed-to-seed variation, which is the whole and only claim.
	///
	/// Clamped, because a table with one outlier figure should not push the whole genre's comp
	/// around, and because a trim is a correction rather than a mix control.
	/// </summary>
	/// <summary>How often a chordal voice plays its genre's flourish over a two-bar window instead
	/// of the figure it is on. One number for every genre: what makes a gesture read as one is that
	/// it is occasional, and how occasional is not something a genre has a strong opinion about —
	/// where genres differ is in what the gesture IS, which is the pattern itself.</summary>
	const float OrnamentChance = 0.22f;

	internal static float DensityTrim( Pattern fig, Pattern[] table )
	{
		if ( table == null || table.Length < 2 ) return 1f;
		float d = fig.Count / (float)fig.LengthTicks;
		if ( d <= 0f ) return 1f;
		float mean = 0f;
		foreach ( var p in table ) mean += p.Count / (float)p.LengthTicks;
		mean /= table.Length;
		float norm = 0f;
		foreach ( var p in table )
		{
			float pd = p.Count / (float)p.LengthTicks;
			norm += pd > 0f ? MathF.Sqrt( mean / pd ) : 1f;
		}
		norm /= table.Length;
		if ( norm <= 0f ) return 1f;
		return Math.Clamp( MathF.Sqrt( mean / d ) / norm, 0.75f, 1.3f );
	}

	/// <summary>How long a comp hit rings.
	///
	/// NOT simply "until the next onset". A figure with uneven gaps then produces a note with an
	/// uneven length every single bar — the "short, longggg" shape that made the backing read as
	/// one repeated cell however varied the figure was. A chord rings for up to two beats and a
	/// stab is a stab; past that the voice is silent and the next hit lands into space, which is
	/// what a played part actually sounds like.</summary>
	static int CompLen( int spanTicks, bool ring )
		=> Math.Max( 1, Math.Min( spanTicks, ring ? Timing.TicksPerBeat * 2 : Timing.TicksPerEighth ) );

	/// <summary>The second chordal voice — the keys/piano/synth layer, where the genre has one.
	/// It never doubles the main voice: rock's organ answers the riff's gaps, country's piano
	/// hits the backbeat the guitar leaves alone, pop's arp moves over a pad that does not.
	/// </summary>
	void RenderKeysVoice( int barTick, int to, int chord, Rng rng, Rng exprRng, bool ornament )
	{
		var hits = (ornament ? _prof.KeysOrnament : _keysFig).Slice( barTick, to, _sectionTick, _feel );
		Trace?.Add( TraceVoice.Keys, hits );
		switch ( _prof.Keys )
		{
			case KeysStyle.HonkyTonk: RenderHonkyTonkBar( hits, chord, rng, exprRng ); break;
			case KeysStyle.Arp: RenderArpBar( hits, chord, rng, exprRng ); break;
			default: RenderKeysStabBar( hits, chord, rng, exprRng ); break;
		}
	}
}
gamah.skafinity / UI/SkafinityMusicPanel.razor
Game library
@using System
@using System.Collections.Generic
@using System.Linq
@using Sandbox
@using Sandbox.UI
@namespace Skafinity
@inherits PanelComponent
@* DontExecuteOnServer: this panel is pure human-client UI (like SkafinityPlayer). A dedicated
   server never renders it, so skip its lifecycle there. On a listen server the host is still a
   player, so the host-client keeps the panel — this only sheds the headless case. *@
@implements Component.DontExecuteOnServer

@*
	The optional drop-in settings board for the Skafinity music engine.

	Add this PanelComponent to a GameObject under a ScreenPanel (or WorldPanel). It finds a
	SkafinityPlayer in the scene (or set Player explicitly) and offers the whole transport as UI —
	you don't have to wire anything. The board's visibility is host-driven: set IsOpen (or call
	Toggle()) from your game — e.g. bind it to a hotkey or your own pause/menu UI. This component
	intentionally ships no launcher of its own, so it imposes nothing on the host's HUD — which also
	means a freshly-dropped panel shows nothing until you bind IsOpen. Run `skafinity_panel` in the
	console to see it before you have (SkafinityCommands.cs).

	The engine needs nothing from this: SkafinityPlayer plays on its own. This board is pure
	convenience for players who want to drive the station rather than tune it in the inspector.

	It is drawn against the WEB WIDGET as its design (web/skafinity-element.js) so the two are one
	product with one set of habits, and the wording and the derived decisions both sides need live in
	SkafinityBoard rather than in either drawing of it. What is deliberately NOT shared is layout:
	Razor and the DOM lay out differently enough that a common description of a row would be a lowest
	common denominator of both.

	Re-theming: the board derives its whole palette from one colour. Set SkafinityTheme.Accent from
	your game (e.g. to your own UI accent) and the board follows; leave it unset and it is neutral
	gray-on-black. SkafinityMusicPanel.razor.scss holds only the layout/type tokens.
*@

<root class="@( IsOpen ? "open" : "" )">
	@if ( IsOpen )
	{
	@{
		var cfg = Player?.EffectiveConfig();
		int genre = cfg?.Genre ?? 0;
		var here = Player?.Playhead() ?? default;
		bool known = here.Duration > 0;
	}
	<div class="board" style="background-color:@SkafinityTheme.Bg;">
		<div class="header">
			<div class="title">MUSIC</div>
			<div class="close" onclick="@Toggle">✕</div>
		</div>

		@* ── Transport ─────────────────────────────────────────────────────────────────────
		   Two rows: the buttons, and the bar that says where in the song they are acting. *@
		<div class="transport">
			<div class="row">
				<div class="btn big" style="@BtnStyle" tooltip="@SkafinityBoard.Copy.PrevTitle"
					 onclick="@( () => Step( -1 ) )">@SkafinityBoard.Copy.Prev</div>
				<div class="btn big primary" style="@BtnOnStyle" tooltip="@SkafinityBoard.Copy.PlayTitle"
					 onclick="@TogglePlay">@( Playing ? SkafinityBoard.Copy.Pause : SkafinityBoard.Copy.Play )</div>
				<div class="btn big" style="@BtnStyle" tooltip="@SkafinityBoard.Copy.NextTitle"
					 onclick="@( () => Step( 1 ) )">@SkafinityBoard.Copy.Next</div>

				<div class="now" style="@LabelStyle">@SkafinityBoard.Copy.NowPlaying<b style="@TextStyle">@( Player?.N ?? 0 )</b></div>

				@* Playback stalled on the song you skipped to — as opposed to the silent background
				   look-ahead, which nobody needs telling about. *@
				@if ( Player?.IsBuffering == true )
				{
					<div class="bufstate" style="@AccentTextStyle">@SkafinityBoard.Copy.Generating( Player?.N ?? 0 )</div>
				}

				<div class="vol right" style="@LabelStyle">
					@SkafinityBoard.Copy.Volume
					<SkafinitySlider Min="@(0f)" Max="@(1.5f)" Step="@(0.01f)" FixedWidth="@(120f)"
									 Value="@( Player?.Volume ?? 1f )"
									 OnValueChanged="@( (float v) => SetVolume( v ) )"></SkafinitySlider>
				</div>
			</div>

			@* The seek bar. The whole song is already in memory, so a scrub is a stream restart on
			   PCM we are holding rather than a fetch — which is why this is a plain slider and not a
			   loading affordance. It goes inert, rather than drawing against a guess, until the song
			   has been rendered and has a length worth stating. *@
			<div class="seek">
				<div class="time" style="@LabelStyle">@SkafinityBoard.Time( ShownTime( here ), known )</div>
				<SkafinitySlider tooltip="@SkafinityBoard.Copy.SeekTitle" Disabled="@( !known )"
								 Min="@(0f)" Max="@(1000f)" Step="@(1f)"
								 Value="@( known ? ShownRatio( here ) * 1000f : 0f )"
								 OnValueChanged="@( (float v) => Scrub( v / 1000f, here.Duration ) )"></SkafinitySlider>
				<div class="time total" style="@LabelStyle">@SkafinityBoard.Time( here.Duration, known )</div>
			</div>
		</div>

		@* ── The seed ──────────────────────────────────────────────────────────────────────
		   TWO copy buttons, because "share this" means one of two things and neither can be
		   recovered from the other: the song, with everything it left to chance written down, or
		   the station as it stands, which keeps rolling for whoever is handed it. *@
		<div class="row seed-bar">
			<TextEntry @ref="_seedEntry" placeholder="@SkafinityBoard.Copy.SeedPlaceholder" class="grow seed-input" />
			<div class="btn primary" style="@BtnOnStyle" onclick="@PlayTyped">@SkafinityBoard.Copy.SeedGo</div>
			<div class="btn" style="@BtnStyle" tooltip="@SkafinityBoard.Copy.CopySongTitle"
				 onclick="@CopySong">@_copySongLabel</div>
			<div class="btn" style="@BtnStyle" tooltip="@SkafinityBoard.Copy.CopyStationTitle"
				 onclick="@CopyStation">@_copyStationLabel</div>
		</div>

		@* ── What plays ────────────────────────────────────────────────────────────────────
		   These are not knob controls — genre, reroll and shuffle change the seed, and tinker only
		   opens the box — so they live on the board itself. Putting them in the mixer made them look
		   like part of it AND hid them from everyone who never opened it. *@
		<div class="row what-plays">
			<div class="label" style="@LabelStyle">@SkafinityBoard.Copy.Genre</div>
			@* The dropdown IS the seed's genre part: "Random" takes it out of the string so every
			   song rolls its own again, and without that entry there is no way back out of a genre
			   once one has been chosen. It reads as selected when nothing is pinned. *@
			<DropDown class="genre" style="@BtnStyle" Value="@GenreValue" Options="@GenreOptions"
					  ValueChanged="@( (string v) => PickGenre( v ) )" />
			<div class="btn" style="@BtnStyle" tooltip="@SkafinityBoard.Copy.RerollTitle"
				 onclick="@RerollStation">@SkafinityBoard.Copy.Reroll</div>
			<div class="btn toggle @( Shuffling ? "on" : "" )" style="@( Shuffling ? BtnOnStyle : BtnStyle )"
				 tooltip="@SkafinityBoard.Copy.ShuffleTitle"
				 onclick="@ToggleShuffle">@( Shuffling ? SkafinityBoard.Copy.ShuffleOn : SkafinityBoard.Copy.ShuffleOff )</div>
			<div class="btn toggle right @( _tinkering ? "on" : "" )" style="@( _tinkering ? BtnOnStyle : BtnStyle )"
				 onclick="@ToggleTinker">@( _tinkering ? SkafinityBoard.Copy.TinkerOpen : SkafinityBoard.Copy.Tinker )</div>
		</div>

		@* ── The knobs ─────────────────────────────────────────────────────────────────────
		   Behind the tinker button. They are the deep end of the toy, and a wall of sliders is
		   otherwise the first thing anybody meets. *@
		@if ( _tinkering )
		{
			<div class="panel vibe" style="@PanelStyle">
				<div class="h2" style="@LabelStyle">@SkafinityBoard.Copy.VibeHeading</div>
				<div class="matrix">
					<div class="mrow mhead">
						<div class="mvoice"></div>
						@foreach ( var h in SkafinityBoard.ColumnHeaders )
						{
							<div class="mcell mhlabel" style="@LabelStyle">@h</div>
						}
					</div>
					@foreach ( var row in SkafinityBoard.Matrix( genre ) )
					{
						<div class="mrow">
							<div class="mvoice">@row.Voice</div>
							@for ( int col = 0; col < SkafinityBoard.ColumnHeaders.Length; col++ )
							{
								var f = row.Cells[col];
								<div class="mcell">
									@if ( f != null )
									{
										@Knob( f, cfg, SkafinityBoard.KnobLabel( f, col ) )
									}
								</div>
							}
						</div>
					}
				</div>

				@* Only when there IS a global knob. They have all been retired to reserved wire slots
				   (tempo to GenreProfile, width and reverb to house config), and a heading over an
				   empty grid reads as a panel that failed to draw something. *@
				@if ( SkafinityBoard.Globals( genre ).Count > 0 )
				{
					<div class="glabel" style="@LabelStyle">@SkafinityBoard.Copy.GlobalHeading</div>
					<div class="global-grid">
						@foreach ( var f in SkafinityBoard.Globals( genre ) )
						{
							<div class="knob-cell">@Knob( f, cfg, f.Name )</div>
						}
					</div>
				}

				@* The only two buttons that act on the sliders, and they are not two dice. 🎲 always
				   moves every knob, because it draws a fresh vibe and PINS it. ↺ is the way back out —
				   dragging a knob pins the whole vibe, so without it one accidental drag turns an
				   endless station into one song forever — and it is off when there is nothing pinned
				   rather than looking like a die that did nothing. *@
				<div class="row vibe-actions">
					<div class="btn" style="@BtnStyle" tooltip="@SkafinityBoard.Copy.VibeRollTitle"
						 onclick="@RerollVibe">@SkafinityBoard.Copy.VibeRoll</div>
					<div class="btn @( VibePinned ? "" : "off" )" style="@BtnStyle"
						 tooltip="@SkafinityBoard.Copy.VibeRandomTitle"
						 onclick="@RollVibe">@SkafinityBoard.Copy.VibeRandom</div>
				</div>
			</div>
		}

		@* ── The playlist ──────────────────────────────────────────────────────────────────
		   Past · now · up next. A row addresses its song by POSITION, which is its slot on the
		   timeline; the number it SHOWS is the song's index in its own station, and under shuffle
		   those are different things. *@
		<div class="panel playlist-panel" style="@PanelStyle">
			<div class="h2" style="@LabelStyle">@SkafinityBoard.Copy.PlaylistHeading</div>
			<div class="playlist">
				@foreach ( var e in Queue() )
				{
					var ee = e;
					<div class="plrow @( e.Current ? "now" : "" ) @( e.Cached ? "cached" : "" ) @( e.Progress >= 0 ? "gen" : "" )"
						 style="@RowStyle( e )">
						<div class="pllabel" onclick="@( () => Player?.SeekTo( ee.Position ) )">
							<div class="plcaret">@SkafinityBoard.RowCaret( e )</div>
							<div class="plhash">@SkafinityBoard.Copy.Hash</div>
							<div class="plnum">@e.N</div>
						</div>
						<div class="plgenre" style="@LabelStyle">@SkafinityBoard.GenreName( e.Genre )</div>
						<div class="plstatus" style="@LabelStyle">
							@if ( e.Progress >= 0 )
							{
								<div class="bar"><div class="bar-fill" style="@BarFillStyle( e.Progress )"></div></div>
							}
							else
							{
								@SkafinityBoard.RowStatus( e )
							}
						</div>
						<div class="pldl" style="@LabelStyle" tooltip="@SkafinityBoard.Copy.ExportTitle( e.N )"
							 onclick="@( () => Save( ee.Position ) )">⬇</div>
					</div>
				}
			</div>
			<div class="row jump" style="@LabelStyle">
				@SkafinityBoard.Copy.JumpTo
				<TextEntry @ref="_jumpEntry" Numeric="@true" class="jump-input" />
				<div class="btn" style="@BtnStyle" onclick="@JumpTo">@SkafinityBoard.Copy.JumpGo</div>
				<div class="btn right" style="@BtnStyle"
					 onclick="@( () => Save( Player?.Position ?? 0 ) )">@( _saving ? SkafinityBoard.Copy.ExportBusy : SkafinityBoard.Copy.Export )</div>
			</div>
		</div>

		@if ( _msg != null )
		{
			<div class="msg" style="@AccentTextStyle">@_msg</div>
		}
	</div>
	}
</root>

@code
{
	/// <summary>The player this panel drives. Leave unset to auto-find a <see cref="SkafinityPlayer"/>
	/// in the scene on start.</summary>
	[Property] public SkafinityPlayer Player { get; set; }

	/// <summary>Whether the settings board is showing. Host-driven — set it from your game (or
	/// call <see cref="Toggle"/>) to wire the board to a hotkey / pause menu / your own button.
	/// This component ships no launcher of its own.</summary>
	/// <remarks>Deliberately NOT a <c>[Property]</c>: this is transient UI state. On a networked
	/// (<c>NetworkMode: Snapshot</c>) GameObject a serialized <c>[Property]</c> rides the late-join
	/// snapshot, so a client joining while the host has the board open would restore
	/// <c>IsOpen = true</c> — leaking the host's UI state and rendering the board open (and unstyled,
	/// since the panel is rebuilt mid-deserialize). Leaving it un-serialized keeps it host-driven
	/// from code while starting <c>false</c> on every client.</remarks>
	public bool IsOpen { get; set; }

	TextEntry _seedEntry;
	TextEntry _jumpEntry;
	bool _seedInit;
	// The station seed this panel last wrote into the box — what tells a stale box from a typed one.
	string _seedShown;
	// Both copy buttons keep their own label so pressing one doesn't report "copied!" on the other.
	string _copySongLabel = SkafinityBoard.Copy.CopySong;
	string _copyStationLabel = SkafinityBoard.Copy.CopyStation;
	string _msg;
	// The knob matrix is closed until asked for. Not a [Property] for the same reason IsOpen is not.
	bool _tinkering;
	bool _saving;

	// A DRAG, held. A slider reports every mouse-move and there is no "let go" event to wait for, so
	// seeking on each report would restart the stream at every pixel. The thumb is therefore followed
	// here and the transport is told once the drag has settled — which is the same one-seek-per-gesture
	// the web gets from listening to `change` rather than `input`.
	const float ScrubSettle = 0.2f;
	float _scrubTo = -1f;
	TimeSince _scrubSince;

	protected override void OnStart()
	{
		Player ??= Scene.GetAllComponents<SkafinityPlayer>().FirstOrDefault();
		if ( Player == null )
			Log.Warning( "SkafinityMusicPanel: no SkafinityPlayer found in the scene — add one (or set Player)." );
	}

	protected override void OnUpdate()
	{
		// The text entry follows the station as it stands — not once at open, but whenever the station
		// moves out from under it. Reroll, a pasted seed and a genre pin all change what the station
		// IS, and a box still showing the previous one reads as a reroll that did nothing.
		//
		// Only ever overwrites text this panel wrote: the moment somebody types, the box is theirs and
		// a background change to the station leaves it alone rather than eating what they were typing.
		if ( !IsOpen ) { _seedInit = false; return; }
		if ( _seedEntry != null )
		{
			var station = Player?.StationSeed ?? "";
			if ( !_seedInit || ( station != _seedShown && _seedEntry.Text == _seedShown ) )
			{
				_seedEntry.Text = station;
				_seedShown = station;
				_seedInit = true;
			}
		}

		// The held scrub, applied once the drag settles. See _scrubTo.
		if ( _scrubTo >= 0f && _scrubSince > ScrubSettle )
		{
			var d = Player?.Playhead().Duration ?? 0;
			if ( d > 0 ) Player?.SeekWithin( _scrubTo * d );
			_scrubTo = -1f;
		}
	}

	// ── Theme bindings ──
	// The palette is runtime (SkafinityTheme), so every fill and themed text colour is bound here as
	// an inline style rather than named in the .scss. The stylesheet owns every border in return —
	// see the header comment there for why the two must not overlap.
	//
	// Neither a rule nor an inline style reaches inside a control this library did not write, which
	// is why the board's slider is one it does (SkafinitySlider) — and why the accent reaches the
	// sliders at all.
	static string LabelStyle => $"color:{SkafinityTheme.TextDim};";
	static string TextStyle => $"color:{SkafinityTheme.Text};";
	static string AccentTextStyle => $"color:{SkafinityTheme.AccentCss};";
	static string BtnStyle => $"background-color:{SkafinityTheme.Cell}; color:{SkafinityTheme.Text};";
	static string BtnOnStyle => $"background-color:{SkafinityTheme.AccentBg}; color:{SkafinityTheme.Text};";
	static string PanelStyle => $"background-color:{SkafinityTheme.Cell};";
	static string BarFillStyle( float p ) => $"width:{SkafinityBoard.Percent( p )}; background-color:{SkafinityTheme.AccentCss};";

	// A playlist row reads as one of three states, brightest first: the song playing now, a song
	// already rendered and waiting, anything else.
	static string RowStyle( SkafinityPlayer.QueueEntry e ) =>
		e.Current ? $"background-color:{SkafinityTheme.AccentBg};"
		: e.Cached ? $"background-color:{SkafinityTheme.CellFillSoft};"
		: "";

	bool Playing => Player != null && !Player.IsPaused;
	bool Shuffling => Player?.Shuffle ?? false;
	bool VibePinned => Player?.VibePinned ?? false;
	bool GenreRolling => Player == null || !Player.GenrePinned;

	/// <summary>Open/close the settings board. Convenience for hosts that want to bind a single
	/// action; you can also set <see cref="IsOpen"/> directly.</summary>
	public void Toggle()
	{
		IsOpen = !IsOpen;
		if ( !IsOpen ) _seedInit = false;
	}

	// ── The playhead ──
	// Mid-drag the thumb the user is holding wins over the clock; every other moment reads the
	// transport. Both halves have to agree or the label counts up while the thumb sits still.
	float ShownRatio( SkafinityPlayer.SongPosition p ) => _scrubTo >= 0f ? _scrubTo : p.Ratio;
	double ShownTime( SkafinityPlayer.SongPosition p ) => _scrubTo >= 0f ? _scrubTo * p.Duration : p.Time;

	void Scrub( float ratio, double duration )
	{
		if ( duration <= 0 ) return;
		_scrubTo = Math.Clamp( ratio, 0f, 1f );
		_scrubSince = 0;
	}

	void TogglePlay() { Player?.TogglePlay(); _msg = null; }
	void Step( int d ) { Player?.StepN( d ); _msg = null; }
	void SetVolume( float v ) { if ( Player != null ) Player.Volume = v; }

	// ── The seed ──
	void PlayTyped()
	{
		if ( Player == null ) { _msg = null; return; }
		// A seed that will not parse leaves playback exactly where it was and says why, rather than
		// starting something adjacent to what was typed.
		_msg = Player.PlaySeed( _seedEntry?.Text, out var error )
			? SkafinityBoard.Copy.Playing( Player.CurrentSeed ) : error;
	}

	// This song, with everything it left to chance written down — whoever is handed it hears THIS,
	// not whatever their own station rolls at that index.
	void CopySong()
	{
		try { Clipboard.SetText( Player?.CurrentSeed ?? "" ); _copySongLabel = SkafinityBoard.Copy.Copied; }
		catch { _copySongLabel = "—"; }
	}

	// The seed as it stands: whatever this player left rolling keeps rolling for them too.
	void CopyStation()
	{
		try { Clipboard.SetText( Player?.StationSeed ?? "" ); _copyStationLabel = SkafinityBoard.Copy.Copied; }
		catch { _copyStationLabel = "—"; }
	}

	// ── What plays ──
	// "" is the Random entry — the genre is not in the seed, so every song rolls its own.
	static readonly List<Option> GenreOptions = BuildGenreOptions();
	static List<Option> BuildGenreOptions()
	{
		var list = new List<Option> { new( SkafinityBoard.Copy.GenreRandom, "" ) };
		for ( int g = 0; g < VibeCodec.GenreCount; g++ )
			list.Add( new Option( VibeCodec.Genres[g], g.ToString() ) );
		return list;
	}
	string GenreValue => GenreRolling ? "" : ( Player?.EffectiveConfig()?.Genre ?? 0 ).ToString();

	void PickGenre( string v )
	{
		if ( Player == null ) return;
		if ( string.IsNullOrEmpty( v ) ) { Player.RollGenre(); _msg = SkafinityBoard.Copy.GenreUnpinned; return; }
		if ( int.TryParse( v, out var g ) ) { Player.SetGenre( g ); _msg = null; }
	}

	// A different SONG, not a different taste: a fresh station at song 0, with anything pinned left
	// pinned.
	void RerollStation() { Player?.RerollStation(); _msg = SkafinityBoard.Copy.NewStation; }

	void ToggleShuffle() { if ( Player != null ) Player.SetShuffle( !Player.Shuffle ); _msg = null; }

	void ToggleTinker() { _tinkering = !_tinkering; }

	// ── The knobs ──
	// One knob: a name/value header over a real slider (or a dropdown, where the field is a choice).
	// The whole layout comes from the library's field metadata for the current genre, so a new genre
	// — or a new knob — is a pure engine change and there is no field table here.
	RenderFragment Knob( VibeCodec.Field f, MusicGen.Config cfg, string label )
	{
		int genre = cfg?.Genre ?? 0;
		int idx = SkafinityBoard.FieldIndex( genre, f );
		float norm = cfg != null ? f.GetNorm( cfg ) : 0f;
		return @<text>
		<div class="knob">
			<div class="knob-head">
				<div class="knob-name" style="@LabelStyle">@label</div>
				<div class="knob-val" style="@AccentTextStyle">@( cfg != null ? f.Display( cfg ) : "" )</div>
			</div>
			@if ( f.Choices != null )
			{
				<DropDown class="knob-select" style="@BtnStyle" Value="@SkafinityBoard.ChoiceIndex( f, norm ).ToString()"
						  Options="@ChoiceOptions( f )"
						  ValueChanged="@( (string v) => SetChoice( idx, f, v ) )" />
			}
			else
			{
				@* Snapped to the same discrete grid the seed encodes (one level per base-36 char), so
				   the slider can only land on values the vibe can actually represent. *@
				<SkafinitySlider Min="@(0f)" Max="@( (float)(VibeCodec.Levels - 1) )" Step="@(1f)"
								 Value="@( MathF.Round( norm * (VibeCodec.Levels - 1) ) )"
								 OnValueChanged="@( (float v) => SetVibe( idx, v / (VibeCodec.Levels - 1) ) )"></SkafinitySlider>
			}
		</div>
	</text>;
	}

	static List<Option> ChoiceOptions( VibeCodec.Field f )
	{
		var list = new List<Option>( f.Choices.Length );
		for ( int k = 0; k < f.Choices.Length; k++ ) list.Add( new Option( f.Choices[k], k.ToString() ) );
		return list;
	}

	void SetChoice( int idx, VibeCodec.Field f, string v )
	{
		if ( int.TryParse( v, out var k ) ) SetVibe( idx, SkafinityBoard.ChoiceNorm( f, k ) );
	}

	void SetVibe( int index, float norm )
	{
		if ( index < 0 ) return;
		Player?.SetVibe( index, norm );
		_msg = null;
	}

	// RerollVibe()'s defaults: the genre and the per-instrument volumes stay — the die over the
	// mixer re-voices the band, it does not swap the band or upend the mix you set.
	void RerollVibe() { Player?.RerollVibe(); _msg = SkafinityBoard.Copy.VibeRolled; }

	void RollVibe()
	{
		if ( Player == null || !Player.VibePinned ) return;   // nothing pinned — nothing to hand back
		Player.RollVibe();
		_msg = SkafinityBoard.Copy.VibeUnpinned;
	}

	// ── The playlist ──
	// How many history / look-ahead entries to show either side of the current song.
	static int QueueBack => 3;
	static int QueueFwd => 5;
	IEnumerable<SkafinityPlayer.QueueEntry> Queue() =>
		Player?.Timeline( QueueBack, QueueFwd ) ?? Enumerable.Empty<SkafinityPlayer.QueueEntry>();

	void JumpTo()
	{
		if ( Player == null ) return;
		if ( int.TryParse( _jumpEntry?.Text, out var p ) ) Player.SeekTo( p );
	}

	// Rendering a song outside the cache takes seconds, so the button says so — a save that looks
	// like it did nothing is a save people press again.
	async void Save( int position )
	{
		if ( Player == null || _saving ) return;
		_saving = true;
		try
		{
			var name = await Player.SaveToFileAsync( position );
			_msg = string.IsNullOrEmpty( name ) ? SkafinityBoard.Copy.SaveFailed : SkafinityBoard.Copy.Saved( name );
		}
		finally { _saving = false; }
	}

	protected override int BuildHash()
	{
		// Fold in the playhead and the queue's cached/generating state so the board animates as the
		// song runs and as songs render. Both are QUANTISED: the seek bar wants to move, but a panel
		// that rebuilds every frame to advance a bar by a pixel costs more than it shows. A fifth of
		// a second is smooth to look at and cheap to draw.
		var q = new HashCode();
		q.Add( IsOpen ); q.Add( Player?.CurrentSeed ); q.Add( Player?.CurrentVibe );
		q.Add( Player?.Enabled ?? true ); q.Add( Player?.Volume ?? 1f );
		q.Add( Player?.GenrePinned ?? false ); q.Add( Player?.VibePinned ?? false );
		q.Add( Player?.Shuffle ?? false ); q.Add( Player?.IsPaused ?? false );
		q.Add( _tinkering ); q.Add( Player?.IsBuffering ?? false ); q.Add( _saving );
		q.Add( _msg ); q.Add( _copySongLabel ); q.Add( _copyStationLabel );
		q.Add( _scrubTo );
		// The palette rides in inline style= values, so the board has to rebuild when the host
		// retints it — nothing else in this hash moves when only SkafinityTheme.Accent changes.
		q.Add( SkafinityTheme.Accent );
		if ( IsOpen )
		{
			var here = Player?.Playhead() ?? default;
			q.Add( (int)(here.Time * 5) ); q.Add( (int)(here.Duration * 5) );
			foreach ( var e in Queue() )
			{
				q.Add( e.N ); q.Add( e.Position ); q.Add( e.Cached ); q.Add( e.Current ); q.Add( e.Genre );
				q.Add( e.Progress >= 0 ? (int)MathF.Round( e.Progress * 20 ) : -1 );
			}
		}
		return q.ToHashCode();
	}
}
Debug: View Raw JSON Response
{
    "TotalCount": 78,
    "Files": [
        {
            "Ident": "gamah.skafinity",
            "Path": "Code/Engine/Drums/Groove.cs",
            "FileName": "Groove.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 341414,
            "IsPrivate": false,
            "Code": "using System;\nusing System.Collections.Generic;\n\nusing static Skafinity.Osc;\n\nnamespace Skafinity;\n\n/// <summary>\n/// One drum groove: what the kick, the snare and the cymbal play.\n///\n/// Grooves used to be five cases in a <c>switch</c>, and rock, country AND punk all resolved to\n/// the same <c>default</c> straight backbeat \u2014 three of six genres playing identical drums under\n/// different guitars. A groove is a set of patterns now, the same way harmony is a set of\n/// tables, and each genre draws from its own.\n///\n/// Cell values: the kick has none (an onset is a kick). A snare cell is 0 for a hit and\n/// <see cref=\"Ghost\"/> for a ghost note. A cymbal cell is 0 for the closed/bow articulation and\n/// <see cref=\"Open\"/> for the open hat / ride bell \u2014 which of the two instruments plays is the\n/// section's hats-or-ride roll, not the groove's business.\n///\n/// WHAT IS MEASURED IS THE TABLE, AND WHAT SHIPS IS ARRANGED FROM IT. Read this before quoting\n/// any number below. The placements were fitted to a played corpus and they are real; the engine\n/// then draws a groove per SECTION and works on its kick and snare against the section's skeleton\n/// (see <see cref=\"MusicGen.ArrangeKit\"/>), so a bar that reaches a listener is a mutation of a\n/// measured pattern rather than the pattern itself. Three things follow and they are separable:\n///\n///   * <b>the tables are measured SEED material.</b> The placements are real, the three\n///     corrections below are still why these tables look the way they do, and the arranger never\n///     invents a gesture the genre does not have.\n///   * <b>what the engine plays is a design call</b>, bounded by the SPINE (see\n///     <see cref=\"SpineOf\"/>) \u2014 the struck backbeat and the downbeat kick, which mutation may not\n///     reach, so a genre's identity survives being arranged.\n///   * <b>the accent weights are untouched and remain measured.</b> Velocity was a separate\n///     question off the same pass and nothing about arranging placements reaches it.\n///\n/// RESTORING THE MEASURED OCCUPANCY WAS CONSIDERED AND LOST, and this is recorded because the\n/// paragraph below is what will make a future session rediscover it. The pass read a DISTRIBUTION\n/// \u2014 what fraction of bars carry each drum at each position \u2014 and then thresholded it to binary\n/// cells, so the variance was measured and thrown away at authoring time; drawing the cells from\n/// those probabilities instead would give bar-to-bar variation that is the corpus's own rather\n/// than anyone's invention, and the near-certain positions would be a genre guard for free.\n/// (<see cref=\"MusicGen.FootOccupancy\"/> is the one place it survives.) It lost on three counts:\n/// it caps variety at whatever the dataset's variance happens to be, it needs a fresh pass over\n/// Groove MIDI that neither this repo nor its tooling contains, and metal is not in the dataset at\n/// all. It is the fallback if free mutation ever turns out to wreck the genres, and it is scoped.\n///\n/// This distinction is <see cref=\"GenreProfile.FillHits\"/>'s, and it is here for the reason that\n/// block gives in its own words: a sentence in this register is READ as a measurement, so leaving\n/// the header saying \"where the hits fall is measured\" would launder an arrangement into a\n/// citation.\n///\n/// WHERE THE TABLES' HITS FALL IS MEASURED, the same way the accent weights in\n/// <see cref=\"GenreProfile\"/> are, off the same source: Google Magenta's Groove MIDI Dataset\n/// (CC BY 4.0; verified 2026-08-02, https://magenta.tensorflow.org/datasets/groove). Method, since\n/// neither the dataset nor the reader is in this repo: fold every note-on of every 4/4 performance\n/// of a style onto one bar at the nearest sixteenth and read each drum's OCCUPANCY per metric\n/// position \u2014 what fraction of bars carry that drum there. Occupancy answers placement; velocity\n/// answered the accents. The two are separate questions off one pass.\n///\n/// The three placements that disagreed with the tables, and what each one moved:\n///   * country hi-hat \u2014 ~84% on the OFFBEAT eighth against ~36% on the beat, while both country\n///     grooves put the cymbal on the pulse and nowhere else. The \"chick\" is the & and the tables\n///     had it on the beat, which is the single largest mismatch the pass turned up.\n///   * rock kick \u2014 far more &-of-1 and &-of-3 than the two-bar backbeat spent, so each bar of the\n///     pair gains one pushed kick.\n///   * punk snare \u2014 measures on very nearly every eighth rather than on 2 and 4 alone. It is the\n///     train beat's vocabulary at punk's tempo: the backbeat is struck and everything between it\n///     is ghosted.\n///\n/// MIND THE SAMPLE SIZES, which are wildly uneven and travel with any figure taken from here: rock\n/// is 6521 bars and settles rock, punk 278, country 120 from two performances. Country's is thin\n/// enough that the 84/36 split is an INDICATION \u2014 it is acted on because the direction is\n/// unambiguous and the table said the opposite, not because 120 bars settle a number.\n/// </summary>\nsealed class DrumGroove\n{\n\tpublic const int Ghost = 1;\n\n\t// \u2500\u2500 The cymbal hand's vocabulary \u2500\u2500\n\t// Open KEEPS THE VALUE 1, so every table above means exactly what it meant when 0-or-open was\n\t// the whole language. A hi-hat is a pedal and a pedal is a distance, so what a cell says is\n\t// how far open \u2014 except for the two articulations that are not a distance at all: the foot\n\t// closing the cymbals with no stick on them, and the foot opening and shutting them again.\n\tpublic const int Open = 1;\n\t/// <summary>Half open: the \"sloshy\" hat. It is not the midpoint of a switch \u2014 see RenderHat's\n\t/// geometric map, which is what makes this a position a foot can actually hold.</summary>\n\tpublic const int Half = 2;\n\t/// <summary>The foot chick: the hat speaking on its own, with no stick involved.</summary>\n\tpublic const int Foot = 3;\n\t/// <summary>Foot splash: opened and shut in one motion.</summary>\n\tpublic const int Splash = 4;\n\n\tpublic string Name { get; init; }\n\tpublic Pattern Kick { get; init; }\n\tpublic Pattern Snare { get; init; }\n\tpublic Pattern Cymbal { get; init; }\n\n\t/// <summary>Extra ghost-note propensity on top of what the pattern names \u2014 the \"busy\" layer's\n\t/// scaling factor for this groove.</summary>\n\tpublic float GhostRate { get; init; } = 1f;\n\n\t/// <summary>Chance of a crash on the section's first downbeat.</summary>\n\tpublic float CrashOnOne { get; init; } = 0.35f;\n\n\t/// <summary>\n\t/// THE SPINE: which of a groove's onsets ARE the genre, and so may not be dropped or moved.\n\t///\n\t/// This is the drums' answer to <see cref=\"CellClass\"/>, and it is what stops arranging the kit\n\t/// from eroding the three measured tells the corpus pass corrected. It is a LAW rather than a\n\t/// per-groove list of ticks, because a list is a table that has to be re-authored every time a\n\t/// groove is added and gets it wrong silently when nobody does:\n\t///\n\t///   * <b>every STRUCK snare</b>. The backbeat is where a genre puts it \u2014 2 and 4 in most of\n\t///     them, 3 alone in pop's half-time \u2014 and a rule phrased in beat numbers would be wrong for\n\t///     whichever genre disagrees. The ghosts around it are the density and stay arrangeable,\n\t///     which is the whole of what punk's snare has to say: strike two, ghost the rest.\n\t///   * <b>every kick ON A BEAT.</b> A KICK ON THE BEAT IS THE PULSE; A KICK OFF IT IS THE PUSH,\n\t///     and the push is the thing a drummer varies. Protecting the bar's first beat alone was\n\t///     not enough and the failure was specific rather than general: beat 1 held at 96\u201397% in\n\t///     every genre while every OTHER anchor eroded \u2014 country's beat 3, half of boom-chick, went\n\t///     missing in 23% of bars, pop's beat 4 in 24%, rock's beat 3 in 15%. The kick count per\n\t///     bar barely moved, so nothing about its level or its density said so; what a listener\n\t///     gets is a kick that flickers where the pulse should be.\n\t///\n\t/// AND A GROOVE'S IDENTITY IS PARTLY WHERE IT DOES NOT PLAY, which a rule about onsets cannot\n\t/// say on its own. The one drop IS the hole on beat 1, so <see cref=\"MusicGen.Add\"/> may not\n\t/// put a kick on a beat either \u2014 same law read the other way round, and without it ska's beat-1\n\t/// occupancy climbed 41% \u2192 45% as one-drop bars quietly acquired the downbeat they are defined\n\t/// by not having. On the beat is the groove; off the beat is the arrangement.\n\t///\n\t/// The cymbal has no spine here because the cymbal is not arranged at all: it is the pulse, and\n\t/// country's hat on the \"and\" \u2014 the largest mismatch the corpus pass found \u2014 is preserved by\n\t/// construction rather than by a rule that could be got wrong.\n\t/// </summary>\n\tpublic static bool[] SpineOf( Pattern p, bool kick, int barTicks )\n\t{\n\t\tif ( p == null ) return null;\n\t\tvar spine = new bool[p.Count];\n\t\tfor ( int i = 0; i < p.Count; i++ )\n\t\t\tspine[i] = kick ? IsPulse( p.TickAt( i ) ) : p.ValueAt( i ) != Ghost;\n\t\treturn spine;\n\t}\n\n\t/// <summary>A tick the kick's spine lives on \u2014 see <see cref=\"SpineOf\"/>. One law, read two\n\t/// ways: an onset here may not be dropped or moved, and an onset may not be ADDED here.\n\t/// </summary>\n\tpublic static bool IsPulse( int tick ) => tick % Timing.TicksPerBeat == 0;\n\n\tconst int R = Harmony.Rest;\n\tstatic Pattern E( params int[] c ) => Pattern.Eighths( c );\n\tstatic Pattern S( params int[] c ) => Pattern.Sixteenths( c );\n\n\t// \u2500\u2500 Ska-punk \u2500\u2500\n\tpublic static readonly DrumGroove[] SkaPunk =\n\t{\n\t\tnew()\n\t\t{\n\t\t\tName = \"one drop\",\n\t\t\t// The one drop: nothing on beat 1 at all. Kick and snare land together on beat 3, and\n\t\t\t// the space where the downbeat should be is the whole point of the feel.\n\t\t\tKick = E( R, R, R, R, 0, R, R, R ),\n\t\t\tSnare = E( R, R, Ghost, R, 0, R, R, R ),\n\t\t\tCymbal = E( 0, 0, 0, 0, 0, 0, 0, Open ),\n\t\t\tGhostRate = 0.8f, CrashOnOne = 0.25f,\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tName = \"steppers\",\n\t\t\t// Steppers: a kick on every beat \u2014 the four-to-the-floor of reggae, and what a ska\n\t\t\t// song reaches for when it wants to drive rather than lope.\n\t\t\tKick = E( 0, R, 0, R, 0, R, 0, R ),\n\t\t\tSnare = E( R, R, 0, R, R, R, 0, R ),\n\t\t\tCymbal = E( 0, 0, 0, 0, 0, 0, 0, Open ),\n\t\t},\n\t};\n\n\t// \u2500\u2500 Rock \u2500\u2500\n\tpublic static readonly DrumGroove[] Rock =\n\t{\n\t\tnew()\n\t\t{\n\t\t\tName = \"backbeat\",\n\t\t\t// Two bars, because a rock backbeat that is byte-identical every bar is a drum\n\t\t\t// machine. Each bar carries one pushed kick and it is a different one: bar 1 pushes the\n\t\t\t// \"and of 1\", bar 2 the \"and of 2\" into the \"and of 3\". Those pushes are the measured\n\t\t\t// shape \u2014 a rock kick spends far more of its bar on &1 and &3 than two anchor hits.\n\t\t\tKick = E( 0, 0, R, R, 0, R, R, R,\n\t\t\t          0, R, R, 0, 0, 0, R, R ),\n\t\t\tSnare = E( R, R, 0, R, R, R, 0, R,\n\t\t\t           R, R, 0, R, R, R, 0, R ),\n\t\t\tCymbal = E( 0, 0, 0, 0, 0, 0, 0, Open ),\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tName = \"driving eights\",\n\t\t\tKick = E( 0, R, R, 0, 0, R, R, R,\n\t\t\t          0, R, R, 0, 0, R, 0, R ),\n\t\t\tSnare = E( R, R, 0, R, R, R, 0, R,\n\t\t\t           R, R, 0, R, R, R, 0, Ghost ),\n\t\t\tCymbal = E( 0, 0, 0, 0, 0, 0, 0, 0 ),\n\t\t\tGhostRate = 1.2f,\n\t\t},\n\t};\n\n\t// \u2500\u2500 Country \u2500\u2500\n\tpublic static readonly DrumGroove[] Country =\n\t{\n\t\tnew()\n\t\t{\n\t\t\tName = \"train beat\",\n\t\t\t// The train beat: a constant running snare, ghosted everywhere except the backbeat,\n\t\t\t// which is the sound of country drumming and did not exist in this engine at all.\n\t\t\tKick = E( 0, R, R, R, 0, R, R, R ),\n\t\t\tSnare = S( Ghost, Ghost, Ghost, Ghost, 0, Ghost, Ghost, Ghost,\n\t\t\t           Ghost, Ghost, Ghost, Ghost, 0, Ghost, Ghost, Ghost ),\n\t\t\t// The hat is on the \"and\". Two bars to hold the measured split without a per-hit roll:\n\t\t\t// 3 of 8 beats carry the hat against 7 of 8 offbeats, which is the 36/84 the dataset\n\t\t\t// reads. On the pulse it was the one thing in the kit contradicting the genre's own\n\t\t\t// accent weight \u2014 country leans on the offbeat and had nothing there to lean on.\n\t\t\tCymbal = E( 0, 0, R, 0, R, 0, R, 0,\n\t\t\t            R, 0, 0, 0, 0, 0, R, R ),\n\t\t\tGhostRate = 0.6f, CrashOnOne = 0.2f,\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tName = \"two beat\",\n\t\t\t// The other country feel: a two-beat \"boom-chick\" where the kit gets out of the way\n\t\t\t// of the bass and the guitar entirely.\n\t\t\tKick = E( 0, R, R, R, 0, R, R, R ),\n\t\t\tSnare = E( R, R, 0, R, R, R, 0, R ),\n\t\t\t// Lighter than the train beat's hat and still offbeat-led: the \"chick\" of boom-chick is\n\t\t\t// the & whichever country feel is playing, and this one just plays fewer of them.\n\t\t\tCymbal = E( 0, 0, R, R, 0, 0, R, Open ),\n\t\t\tGhostRate = 0.5f, CrashOnOne = 0.15f,\n\t\t},\n\t};\n\n\t// \u2500\u2500 Metal \u2500\u2500\n\tpublic static readonly DrumGroove[] Metal =\n\t{\n\t\tnew()\n\t\t{\n\t\t\tName = \"double kick\",\n\t\t\t// DOUBLE KICK IS A BURST, NOT A SETTING. One bar of unbroken sixteenths looped for\n\t\t\t// three minutes is ~13 hits a second with nothing ever changing, which is why it read\n\t\t\t// as a blast beat at every tempo and every subdivision: the tell is not the rate, it\n\t\t\t// is that the rate never moves. A drummer rides an ordinary kick pattern and stands on\n\t\t\t// the double pedal under a riff \u2014 for a beat into a bar line, for a bar at the top of\n\t\t\t// a phrase \u2014 and the contrast is the whole effect.\n\t\t\t//\n\t\t\t// Four bars, because Pattern carries its own length: bars 1 and 3 are a played metal\n\t\t\t// kick, bar 2 bursts over its last beat, and bar 4 is the full double-kick bar the\n\t\t\t// phrase turns around on. Same one object, no new mechanism.\n\t\t\tKick = S( 0, R, R, R, 0, R, R, 0, 0, R, R, R, 0, R, 0, R,\n\t\t\t          0, R, R, R, 0, R, R, 0, 0, R, R, R, 0, 0, 0, 0,\n\t\t\t          0, R, R, R, 0, R, R, 0, 0, R, R, R, 0, R, 0, R,\n\t\t\t          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ),\n\t\t\tSnare = E( R, R, 0, R, R, R, 0, R ),\n\t\t\tCymbal = E( 0, 0, 0, 0, 0, 0, 0, 0 ),\n\t\t\t// The kick no longer fills every sixteenth, so the busy layer has somewhere to sit\n\t\t\t// again \u2014 but only just: metal's kit is a wall by design and the ghosts are the mortar.\n\t\t\tGhostRate = 0.2f, CrashOnOne = 0.55f,\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tName = \"thrash\",\n\t\t\t// Kick on every eighth under a snare that answers it \u2014 faster to read than the\n\t\t\t// double-kick wall, and it leaves the snare somewhere to go.\n\t\t\tKick = E( 0, 0, 0, 0, 0, 0, 0, 0 ),\n\t\t\tSnare = E( R, R, 0, R, R, R, 0, 0 ),\n\t\t\tCymbal = E( 0, 0, 0, 0, 0, 0, 0, 0 ),\n\t\t\tGhostRate = 0.3f, CrashOnOne = 0.5f,\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tName = \"two-step\",\n\t\t\t// The same beat punk gets, and deliberately the same: bum-tis-bumbum-tis is played in\n\t\t\t// both genres and inventing a heavier variant for metal would be answering a question\n\t\t\t// nobody asked. What separates the two here is everything around it \u2014 tempo, kit, the\n\t\t\t// ghost rate below, and the riff on top.\n\t\t\t//\n\t\t\t//         1   e   &   a   2   e   &   a\n\t\t\t//   kick  K   .   .   .   K   K   .   .\n\t\t\t//   snare .   .   S   .   .   .   S   .\n\t\t\t//\n\t\t\t// A separate OBJECT because no two genres may share a groove (the suite asserts it),\n\t\t\t// which is a rule about tables accidentally converging rather than about two genres\n\t\t\t// never playing the same rhythm.\n\t\t\t//\n\t\t\t// WEIGHTED BEHIND EACH GENRE'S OWN GROOVE, on purpose. This one was added because a\n\t\t\t// listener said it was missing, and the check on that kind of change is not whether the\n\t\t\t// reason was good \u2014 it was, the beat really was absent from both tables \u2014 but whether\n\t\t\t// the WEIGHT came from the same evidence. It did not: joint-top billing was a choice,\n\t\t\t// and it pushed \"eighth drive\", which this file calls the punk engine, from 60% of punk\n\t\t\t// songs to 37%. A genre's signature stays its most common groove and a new arrival\n\t\t\t// earns its share; ~29% is present without displacing anything.\n\t\t\tKick = S( 0, R, R, R, 0, 0, R, R,\n\t\t\t          0, R, R, R, 0, 0, R, R ),\n\t\t\tSnare = S( R, R, 0, R, R, R, 0, R,\n\t\t\t           R, R, 0, R, R, R, 0, R ),\n\t\t\tCymbal = E( 0, 0, 0, 0, 0, 0, 0, 0 ),\n\t\t\tGhostRate = 0.25f, CrashOnOne = 0.5f,\n\t\t},\n\t};\n\n\t// \u2500\u2500 Punk \u2500\u2500\n\tpublic static readonly DrumGroove[] Punk =\n\t{\n\t\tnew()\n\t\t{\n\t\t\tName = \"eighth drive\",\n\t\t\t// The punk engine: eighth-note ride/snare drive at speed. It is not a backbeat with\n\t\t\t// the tempo turned up \u2014 the cymbal hand never stops and the kick pushes every beat.\n\t\t\t//\n\t\t\t// The snare hand does not stop either, which is what the measurement says and what the\n\t\t\t// two-hits-a-bar backbeat could not be: 2 and 4 are struck and every eighth between\n\t\t\t// them is ghosted. The energy gate on ghost cells thins it back toward the bare\n\t\t\t// backbeat in a quiet section, so the density is the section's rather than the table's.\n\t\t\tKick = E( 0, R, 0, R, 0, R, 0, R ),\n\t\t\tSnare = E( Ghost, Ghost, 0, Ghost, Ghost, Ghost, 0, Ghost ),\n\t\t\tCymbal = E( 0, 0, 0, 0, 0, 0, 0, 0 ),\n\t\t\tGhostRate = 0.4f, CrashOnOne = 0.45f,\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tName = \"d-beat\",\n\t\t\t// Early punk, and it stays as it is. Notations of the d-beat vary in where the kick's\n\t\t\t// offbeats sit and this is a legitimate one; it is also NOT the sixteenth gallop the\n\t\t\t// \"two-step\" entry below carries, which is the beat this table was actually missing.\n\t\t\tKick = E( 0, R, R, 0, R, 0, R, R,\n\t\t\t          0, R, R, 0, R, 0, R, R ),\n\t\t\tSnare = E( R, R, 0, R, R, R, 0, R,\n\t\t\t           R, R, 0, R, R, R, 0, Ghost ),\n\t\t\tCymbal = E( 0, 0, 0, 0, 0, 0, 0, 0 ),\n\t\t\tGhostRate = 0.5f, CrashOnOne = 0.5f,\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tName = \"two-step\",\n\t\t\t// THE PUNK ENGINE'S OTHER GEAR: kick on the beat, snare on the \"&\", and the kick\n\t\t\t// doubled at the SIXTEENTH going into every other beat \u2014\n\t\t\t//\n\t\t\t//         1   e   &   a   2   e   &   a\n\t\t\t//   kick  K   .   .   .   K   K   .   .\n\t\t\t//   snare .   .   S   .   .   .   S   .\n\t\t\t//\n\t\t\t// \u2014 repeating twice a bar. Drummers call it the two-step or the skank beat; it is not\n\t\t\t// the d-beat (that keeps its snare on 2 and 4, and has its own entry above).\n\t\t\t//\n\t\t\t// IT IS A 2-BEAT CELL AND THAT IS THE WHOLE POINT. The same figure written over four\n\t\t\t// beats \u2014 kick, snare, doubled kick on 3, snare \u2014 is the identical pattern counted at\n\t\t\t// half the rate, and at this genre's tempo that puts the double every 1.4 s instead of\n\t\t\t// every 0.7 s. It reads as an ordinary rock beat rather than as punk. This engine has\n\t\t\t// been caught by exactly that ambiguity before (see the ska tempo block in\n\t\t\t// GenreProfile): a rhythm means nothing until you say which pulse it is counted against.\n\t\t\tKick = S( 0, R, R, R, 0, 0, R, R,\n\t\t\t          0, R, R, R, 0, 0, R, R ),\n\t\t\tSnare = S( R, R, 0, R, R, R, 0, R,\n\t\t\t           R, R, 0, R, R, R, 0, R ),\n\t\t\tCymbal = E( 0, 0, 0, 0, 0, 0, 0, 0 ),\n\t\t\tGhostRate = 0.35f, CrashOnOne = 0.4f,\n\t\t},\n\t};\n\n\t// \u2500\u2500 Pop \u2500\u2500\n\tpublic static readonly DrumGroove[] Pop =\n\t{\n\t\tnew()\n\t\t{\n\t\t\tName = \"four on the floor\",\n\t\t\tKick = E( 0, R, 0, R, 0, R, 0, R ),\n\t\t\tSnare = E( R, R, 0, R, R, R, 0, R ),\n\t\t\tCymbal = E( 0, Open, 0, Open, 0, Open, 0, Open ),\n\t\t\tGhostRate = 0.5f, CrashOnOne = 0.3f,\n\t\t},\n\t\tnew()\n\t\t{\n\t\t\tName = \"half-time backbeat\",\n\t\t\t// The other modern pop feel: the backbeat falls on 3 alone, which halves the pulse\n\t\t\t// without touching the tempo.\n\t\t\tKick = E( 0, R, R, 0, R, R, 0, R ),\n\t\t\tSnare = E( R, R, R, R, 0, R, R, R ),\n\t\t\tCymbal = S( 0, R, 0, 0, 0, R, 0, 0, 0, R, 0, 0, 0, R, 0, Open ),\n\t\t\tGhostRate = 0.7f, CrashOnOne = 0.35f,\n\t\t},\n\t};\n}\n\n// \u2500\u2500 What a fill is made of \u2500\u2500\n// Every fill this engine ever played was the same object: `per` evenly-spaced, equally-loud\n// hits in EVERY beat of the span, with a floor of four. A two-bar fill was 32 hits that never\n// stopped and never moved, which is the blast-beat read \u2014 nothing about it was fast, it simply\n// occupied every subdivision it could reach. Three things were missing and they are separable.\n//\n// DENSITY, and it is measured. A real rock fill averages 13.2 hits per bar across the whole\n// kit (204 bars of fill performance in the Groove MIDI Dataset \u2014 see DrumGroove's header for\n// the source and the method), against a FLOOR here of 16 and a 32nd branch of 32. The quietest\n// fill this engine could play was already busier than the average real one. The same histogram\n// says what the shape of a bar is: the \"e\" and the \"a\" carry about 0.62 of the occupancy the\n// beats and the \"&\"s do, so a fill is EIGHTHS WITH SIXTEENTH ORNAMENT, not a sixteenth grid.\n// Density is per genre now, like everything else about the kit (GenreProfile.FillHits).\n//\n// SHAPE, which is what `per` structurally could not express, being one number applied to every\n// beat. See FillShape.\n//\n// DYNAMICS. RenderFill called the kit voices directly and so was the one part of the engine\n// with no accent pattern and no energy scaling \u2014 a straight exception to the rule that every\n// voice routes its level through NoteGain. An even stream of equally-loud hits reads as a wall\n// however few of them there are, which is why this is not just a density fix.\n\n/// <summary>The SHAPE of a fill \u2014 where its hits sit inside the span. This is the half of a\n/// fill that a density number cannot say, and the reason there was only ever one fill.</summary>\nenum FillShape\n{\n\t/// <summary>Sparse at the start and filling up into the downbeat it lands on \u2014 a bar that\n\t/// empties itself and then accelerates out of the hole. The commonest fill there is.</summary>\n\tRamp,\n\t/// <summary>Even across the whole span: the roll. What every fill used to be, kept because\n\t/// it is a real shape and metal is mostly made of it.</summary>\n\tRolling,\n\t/// <summary>Space, then a flurry over the last beat. THIS IS WHERE THE THIRTY-SECOND LIVES\n\t/// \u2014 a pickup is short enough to be played and short enough to stay a gesture, which is\n\t/// exactly what a bar of unbroken 32nds is not.</summary>\n\tPickup,\n\t/// <summary>Two or three hits with air around them: the tom figure, the single flam, the\n\t/// bar that does almost nothing. A fill is allowed to be a gesture.</summary>\n\tGesture,\n}\n\n\n// The kit's patterns: which drum lands where. The per-song groove, the section's energy, and\n// the phrase-end fill.\n//\n// Part of the MusicGen engine \u2014 see MusicGen.cs.\n\npublic sealed partial class MusicGen\n{\n\t// \u2500\u2500 Drums \u2500\u2500\n\t// Render one bar of kit off the song's groove. `fillTick` is where a fill takes over (the\n\t// bar's end tick if there is none), so the groove simply stops there and the fill owns the\n\t// rest \u2014 which is what lets a fill be anything from one beat to two bars.\n\tvoid RenderDrumBar( int barTick, int barTicks, int fillTick, Rng noise )\n\t{\n\t\t// Knob ceiling was too frantic: scale so DRUM BUSY 100% reads as the old 75%.\n\t\tfloat busy = Math.Clamp( _c.DrumBusy, 0f, 1f ) * 0.75f * _groove.GhostRate;\n\t\tint to = Math.Min( barTick + barTicks, fillTick );\n\t\tif ( to <= barTick ) return;\n\n\t\t// The cymbal hand. Which instrument it is was decided per section (_ride); the groove\n\t\t// only says where the hits land and which are \"open\". A thin section HALVES the cymbal\n\t\t// pattern rather than playing it quieter \u2014 that is what makes a verse read as a verse and\n\t\t// a breakdown as a breakdown.\n\t\t//\n\t\t// Half the ONSETS, not \"everything off the beat\". Those were the same rule while every\n\t\t// groove's cymbal sat on the pulse, and they stop being the same the moment one does not:\n\t\t// country's hat is on the \"and\", so dropping the offbeats there does not thin the kit, it\n\t\t// deletes the hi-hat from every verse in the genre. Alternate onsets thin any pattern by\n\t\t// half wherever it sits, and for a plain eighth-note cymbal it is exactly what the old rule\n\t\t// did.\n\t\t// A CRASH ON THE SECTION'S FIRST DOWNBEAT. Every groove has carried a CrashOnOne since the\n\t\t// tables were written and nothing ever read it, so no crash landed on any downbeat in the\n\t\t// engine \u2014 only at the end of a fill and the end of a song. It is one draw, on the one bar\n\t\t// it can apply to, so it costs the same from every section's stream.\n\t\t// NOT ON THE SONG'S OWN FIRST BAR. A crash PUNCTUATES a transition \u2014 it is the drummer\n\t\t// marking the seam between one section and the next, and every other place this fires has\n\t\t// one. Bar 1 of the intro has nothing behind it to mark, so what lands there is three\n\t\t// seconds of cymbal wash over the sparsest section in the song, belonging to no groove and\n\t\t// answering nothing that was played. It reads as a cymbal that was already ringing when the\n\t\t// song started, which is exactly what it is.\n\t\t//\n\t\t// The roll still happens, so a genre's draw count does not depend on which bar it is on.\n\t\tif ( barTick == _sectionTick && noise.Chance( _groove.CrashOnOne ) && barTick > 0 )\n\t\t\tRenderCrashCym( _time.TickToSample( barTick ),\n\t\t\t\t_c.CrashVol * KitGain( barTick, 1f, 0.5f ), _crashBright, dark: false );\n\n\t\tbool sparse = _energy < 0.4f;\n\t\t// THINNED FIRST, THEN PLAYED, and the slice runs ONE BEAT PAST the bar. An open hat is\n\t\t// choked by the next hit the drummer actually plays: half the onsets means half the\n\t\t// chokes too, so the thinning cannot happen inside the playing loop \u2014 and the hit that\n\t\t// chokes an \"and of 4\" is in the next bar, which is what the lookahead is for. Only the\n\t\t// hits inside the bar are played; the rest are read.\n\t\tvar cym = _groove.Cymbal.Slice( barTick, to + Timing.TicksPerBeat, _sectionTick, _feel );\n\t\tvar play = new List<Hit>();\n\t\tfor ( int i = 0, kept = 0; i < cym.Count; i++ )\n\t\t\tif ( !(sparse && (kept++ & 1) == 1) ) play.Add( cym[i] );\n\n\t\tfor ( int i = 0; i < play.Count; i++ )\n\t\t{\n\t\t\tvar h = play[i];\n\t\t\tif ( h.Tick >= to ) break;\n\t\t\tTrace?.Add( TraceVoice.Cymbal, h.Tick );\n\t\t\tint at = _time.TickToSample( h.Tick );\n\t\t\tint next = i + 1 < play.Count ? _time.TickToSample( play[i + 1].Tick ) : int.MaxValue;\n\t\t\tint cell = h.Value;\n\t\t\t// The genre's own accent weight decides how a hat off the beat sits against one on it.\n\t\t\t// A flat 0.75 was a house rule where the measurement is per genre and disagrees in both\n\t\t\t// directions \u2014 country and ska lean ON the offbeat, pop's programmed kit buries it.\n\t\t\tfloat kit = KitGain( h.Tick, h.Vel, 0.55f );\n\t\t\tfloat amp = _c.HatVol * kit;\n\t\t\tif ( _crashRide )\n\t\t\t\t// The technique rather than a second cymbal: the hand moves onto a crash and rides\n\t\t\t\t// it, so the open cell is the accent it leans on rather than an open hat.\n\t\t\t\t// Crash-riding is a LIFT \u2014 the hand moves onto the loudest thing in the kit and the\n\t\t\t\t// whole section rises. Measured, it was landing within 0.2 dB of an ordinary ride,\n\t\t\t\t// which is the technique costing a cymbal and buying nothing.\n\t\t\t\tRenderCrashCym( at, _c.CrashVol * kit * (cell == DrumGroove.Open ? 0.67f : 0.44f),\n\t\t\t\t\t_crashDark, dark: true, next, CymbalBands.RestrikeTau );\n\t\t\telse if ( _ride )\n\t\t\t\t// The bell is a CELL now. It used to be positional \u2014 every quarter note was a\n\t\t\t\t// bell, whatever the groove said \u2014 while the open cell a riding section was handed\n\t\t\t\t// fell through to a hi-hat, so the one thing the pattern said about the cymbal\n\t\t\t\t// hand was the one thing the ride ignored.\n\t\t\t\t// EVERY STROKE DAMPS THE ONE BEFORE IT (see RenderCymbal's chokeFloor). A cymbal\n\t\t\t\t// struck eight times a bar is not eight cymbals summed: the stick is on the metal.\n\t\t\t\tRenderRideCym( at, amp * RideStroke( h.Tick - barTick ),\n\t\t\t\t\tcell == DrumGroove.Open ? _rideBell : _rideBow, next, CymbalBands.RestrikeTau );\n\t\t\telse\n\t\t\t\tRenderHat( at, HatOpenness( cell ), amp, noise, HatFor( cell ),\n\t\t\t\t\tRings( cell ) ? next : int.MaxValue );\n\n\t\t\t// Busy fills the gaps with quieter sixteenth chatter.\n\t\t\tif ( cell == 0 && !sparse && noise.Chance( busy ) )\n\t\t\t{\n\t\t\t\tint sixAt = _time.TickToSample( h.Tick + Timing.TicksPerEighth / 2 );\n\t\t\t\tif ( _crashRide ) RenderCrashCym( sixAt, _c.CrashVol * kit * 0.22f, _crashDark, true );\n\t\t\t\telse if ( _ride ) RenderRideCym( sixAt, amp * 0.4f * RideStroke( h.Tick + Timing.TicksPerEighth / 2 - barTick ), _rideBow );\n\t\t\t\telse RenderHat( sixAt, 0f, amp * 0.4f, noise, _hatTone );\n\t\t\t}\n\t\t}\n\n\t\t// The pedal's own part, under a section whose hands are on the ride (see FootOccupancy).\n\t\t// The pattern was drawn once for the section; a foot keeps a figure the way a hand does.\n\t\tif ( (_ride || _crashRide) && _footCells != 0 )\n\t\t\tfor ( int i = 0; i < 8; i++ )\n\t\t\t{\n\t\t\t\tif ( (_footCells & (1 << i)) == 0 ) continue;\n\t\t\t\tint t = barTick + i * Timing.TicksPerEighth;\n\t\t\t\tif ( t >= to ) break;\n\t\t\t\tRenderHat( _time.TickToSample( t ), 0f, _c.HatVol * KitGain( t, 0.7f, 0.45f ),\n\t\t\t\t\tnoise, _footTone );\n\t\t\t}\n\n\t\t// THE KICK READS ITS VELOCITY. It used to discard the cell's Vel and take no metric accent\n\t\t// and no energy scaling at all, so metal's sixteenth double-kick was the identical\n\t\t// waveform at the identical level sixteen times a bar \u2014 which is what machine-gunning is.\n\t\t// Its depth is under the cymbal hand's and near the fill's: the kick is the floor of the\n\t\t// groove and should breathe least.\n\t\tvar kicks = _kickFig.Slice( barTick, to, _sectionTick, _feel );\n\t\tTrace?.Add( TraceVoice.Kick, kicks );\n\t\tforeach ( var h in kicks )\n\t\t\tRenderKick( _time.TickToSample( h.Tick ), noise, KitGain( h.Tick, h.Vel, 0.30f ),\n\t\t\t\t_kickTone, 0f );\n\n\t\t// The kick's own humanising. A groove pattern is exact, and a drummer is not: KICK SYNC is\n\t\t// the chance of a stray extra kick pushing into the following beat, rolled per bar so the\n\t\t// groove breathes instead of stamping the identical bar out for eight bars running.\n\t\tif ( _c.KickSyncChance > 0f )\n\t\t\tfor ( int t = barTick + Timing.TicksPerEighth; t < to; t += Timing.TicksPerBeat )\n\t\t\t\tif ( noise.Chance( _c.KickSyncChance * (0.4f + busy) * _energy ) )\n\t\t\t\t\tRenderKick( _time.TickToSample( t ), noise, KitGain( t, 0.85f, 0.30f ),\n\t\t\t\t\t\t_kickTone, 0f );\n\n\t\tforeach ( var h in _snareFig.Slice( barTick, to, _sectionTick, _feel ) )\n\t\t{\n\t\t\tbool ghost = h.Value == DrumGroove.Ghost;\n\t\t\t// The groove's own ghost notes thin out with the section rather than hammering a\n\t\t\t// verse as hard as a chorus.\n\t\t\tif ( ghost && !noise.Chance( 0.35f + 0.65f * _energy ) ) continue;\n\t\t\tTrace?.Add( TraceVoice.Snare, h.Tick, !ghost );\n\t\t\tRenderSnare( _time.TickToSample( h.Tick ), noise, ghost );\n\t\t}\n\n\t\t// Extra ghosts / toms between the groove's own hits: the \"busy\" layer. A groove that\n\t\t// already fills its own gaps scales this down through GhostRate rather than being\n\t\t// special-cased here.\n\t\tif ( busy > 0f && !sparse )\n\t\t\tfor ( int t = barTick; t < to; t += Timing.TicksPerEighth / 2 )\n\t\t\t{\n\t\t\t\tif ( !noise.Chance( _c.GhostSnareChance * busy * 0.5f ) ) continue;\n\t\t\t\tint at = _time.TickToSample( t );\n\t\t\t\t// The busy layer's toms are the two RACK toms answering each other \u2014 the drums a\n\t\t\t\t// hand can reach without leaving the groove. Which two they are is the kit's, not\n\t\t\t\t// a pair of frequencies picked here (see TomKit).\n\t\t\t\tif ( noise.Chance( (1f - _drumTone) * 0.5f ) )\n\t\t\t\t\tRenderTom( at, _tomKit, (t / (Timing.TicksPerEighth / 2)) & 1, noise,\n\t\t\t\t\t\tKitGain( t, 0.7f, 0.4f ), TomTone.Default );\n\t\t\t\telse RenderSnare( at, noise, true );\n\t\t\t}\n\t}\n\n\t/// <summary>\n\t/// THE FOOT'S OWN PART: how often the pedal closes the hi-hat, per eighth of the bar, while\n\t/// the hands are on the ride. A riding section used to silence the hi-hat completely, and the\n\t/// hat is the one voice in the kit that does not need a hand.\n\t///\n\t/// MEASURED, off the same source as the grooves and the accent weights (Google Magenta's\n\t/// Groove MIDI Dataset \u2014 see DrumGroove's header). Method: over the 472 4/4 performances,\n\t/// split by whether the ride carries the pulse (a ride hit at least every four beats), count\n\t/// note-ons of the PEDAL hi-hat (GM 44) and fold their positions onto one bar at the eighth.\n\t/// Two things came out of it and only one was expected:\n\t///\n\t///   * the foot IS busier when the hands are away \u2014 2.74 pedal hits per bar over 12220 riding\n\t///     bars, against 1.92 over 8519 bars where the hands are on the hat;\n\t///   * but it is NOT \"2 and 4\". Those are the two peaks (16.2% and 17.9% of hits) and the\n\t///     downbeat is a third (13.1%), while every remaining eighth still carries 9.6\u201311.6%.\n\t///     Two chicks a bar on the backbeat is a third of what a drummer's foot actually does,\n\t///     and it is the part that is easiest to assume you already know.\n\t///\n\t/// These are the per-eighth probabilities that reproduce both numbers: each share of the hits\n\t/// times the 2.74 they are shared out of. The pattern is drawn ONCE PER SECTION from them\n\t/// rather than rolled per bar, because a drummer's foot keeps a figure the same way a hand\n\t/// does \u2014 the marginals are what a performance averages to, not what it decides every bar.\n\t/// </summary>\n\tstatic readonly float[] FootOccupancy =\n\t\t{ 0.36f, 0.26f, 0.44f, 0.32f, 0.31f, 0.28f, 0.49f, 0.28f };\n\n\t/// <summary>How hard a ride stroke is, by where it lands. A drummer's \"and\" is a much lighter\n\t/// stroke than the beat; the genre's accent weight alone (rock's offbeat is 1.7 dB down) leaves\n\t/// eight near-equal strokes a bar, and eight near-equal strokes is a WALL however good each one\n\t/// sounds. Pulling the offbeats back was the single most effective change in the whole cymbal\n\t/// exercise \u2014 more than any edit to the voice itself. Suspect the pattern before the timbre.\n\t/// </summary>\n\tstatic float RideStroke( int tickInBar )\n\t\t=> tickInBar % Timing.TicksPerBeat == 0 ? 1f : 0.5f;\n\n\t// \u2500\u2500 The cymbal hand's cells \u2500\u2500\n\t// What a cymbal cell means, once it can mean more than \"open or not\". Openness is a distance\n\t// and the two foot articulations are not distances at all, which is why the tone and the\n\t// position are two lookups rather than one.\n\n\tstatic float HatOpenness( int cell ) => cell switch\n\t{\n\t\tDrumGroove.Open => 1f,\n\t\tDrumGroove.Half => 0.5f,\n\t\tDrumGroove.Splash => 1f,\n\t\t_ => 0f,\n\t};\n\n\tHatTone HatFor( int cell ) => cell switch\n\t{\n\t\tDrumGroove.Foot => _footTone,\n\t\tDrumGroove.Splash => HatTone.Splash,\n\t\t_ => _hatTone,\n\t};\n\n\t/// <summary>Whether this cell leaves the hat RINGING \u2014 i.e. whether there is anything for the\n\t/// next hit's foot to choke. A chick and a splash have already closed the cymbals.</summary>\n\tstatic bool Rings( int cell ) => cell == DrumGroove.Open || cell == DrumGroove.Half;\n\n\t// \u2500\u2500 Fills \u2500\u2500\n\t// A fill is a span, not \"the last beat of the bar\". Length is a weighted draw \u2014 a beat most\n\t// of the time, occasionally a whole bar or two \u2014 and the long ones are GATED to the\n\t// boundaries that earn them (into a final chorus, out of a breakdown), because a two-bar\n\t// fill at every phrase end is not a fill, it is the arrangement.\n\t//\n\t// Returns the tick the fill starts at, so the groove above knows where to stop.\n\tint FillStart( int barTick, int barTicks, bool bigBoundary, Rng rng )\n\t{\n\t\tfloat r = rng.Next();\n\t\tint span;\n\t\tif ( r < 0.55f ) span = Timing.TicksPerBeat;                       // one beat\n\t\telse if ( r < 0.80f || !bigBoundary ) span = Timing.TicksPerBeat * 2; // two beats (from 3)\n\t\telse if ( r < 0.95f ) span = barTicks;                             // a whole bar\n\t\telse span = barTicks * 2;                                          // two bars\n\n\t\t// The fill ends on the bar line it is leading into, so a longer one simply starts\n\t\t// earlier \u2014 two beats start on beat 3, two bars start in the bar before.\n\t\treturn Math.Max( barTick - barTicks, barTick + barTicks - span );\n\t}\n\n\tstatic readonly FillShape[] FillShapeTable =\n\t\t{ FillShape.Ramp, FillShape.Rolling, FillShape.Pickup, FillShape.Gesture };\n\n\t// Occupancy per grid cell within one beat, relative to the beat itself \u2014 the measured shape a\n\t// bar of fill has. The flurry's grid is the same idea at 32nds, with the eighths inside it\n\t// still carrying the weight, so an acceleration still lands on the beats it passes.\n\tstatic readonly float[] FillStraight = { 1f, 0.62f, 1f, 0.62f };\n\tstatic readonly float[] FillTriplet = { 1f, 0.62f, 0.62f };\n\tstatic readonly float[] FillFlurry = { 1f, 0.5f, 0.62f, 0.5f, 1f, 0.5f, 0.62f, 0.5f };\n\n\t/// <summary>How many cells a fill rolls for per beat, whatever grid it is actually on \u2014 the\n\t/// triplet roll, the flurry and the straight sixteenths all pull the same number of values.\n\t/// A knob (TRIPLET here) must never decide how much of the stream a fill spends.</summary>\n\tconst int FillCells = 8;\n\n\t/// <summary>The scale factor that turns a grid's position weights into per-cell probabilities\n\t/// summing to <paramref name=\"hitsPerBar\"/>.</summary>\n\tstatic float FillCellK( float hitsPerBar, float[] grid )\n\t{\n\t\tfloat perBar = 0f;\n\t\tforeach ( var w in grid ) perBar += w;\n\t\treturn hitsPerBar / (perBar * 4f);\n\t}\n\n\t/// <summary>The per-cell probabilities a density target turns into on a grid.\n\t///\n\t/// A WATER-FILL RATHER THAN ONE SCALE FACTOR, and that is the difference between a fill getting\n\t/// denser and a fill getting louder. The beats reach certainty long before a high target does,\n\t/// so a flat scale silently drops everything past that point \u2014 metal asked for 14 hits a bar\n\t/// and would have played 13.4 whatever number it wrote down. What a busier drummer actually\n\t/// adds is ORNAMENT, the \"e\" and the \"a\", so the excess goes there. The grid's real ceiling is\n\t/// four hits a beat, and no genre is near it.</summary>\n\tstatic float[] FillChances( float hitsPerBar, float[] grid )\n\t{\n\t\tvar p = new float[grid.Length];\n\t\tfloat want = hitsPerBar / 4f;                          // per beat\n\t\tfloat k = FillCellK( hitsPerBar, grid );\n\t\tfor ( int pass = 0; pass < 6; pass++ )\n\t\t{\n\t\t\tfloat got = 0f, room = 0f;\n\t\t\tfor ( int i = 0; i < p.Length; i++ ) { p[i] = Math.Clamp( k * grid[i], 0f, 1f ); got += p[i]; }\n\t\t\tfor ( int i = 0; i < p.Length; i++ ) if ( p[i] < 1f ) room += grid[i];\n\t\t\tif ( want - got < 1e-4f || room <= 0f ) break;\n\t\t\tk += (want - got) / room;\n\t\t}\n\t\treturn p;\n\t}\n\n\t/// <summary>What a density target of <paramref name=\"hitsPerBar\"/> actually plays on the\n\t/// straight grid. The engine suite asserts each genre's target against this: a target the model\n\t/// cannot reach is a number that quietly buys nothing.</summary>\n\tinternal static float FillDensityOnGrid( float hitsPerBar )\n\t{\n\t\tfloat hits = 0f;\n\t\tforeach ( var p in FillChances( hitsPerBar, FillStraight ) ) hits += p;\n\t\treturn hits * 4f;\n\t}\n\n\t/// <summary>How dense the fill is at this point in its span, as a multiplier on the genre's\n\t/// target. <paramref name=\"u\"/> is 0 on the fill's first beat and 1 on its last.</summary>\n\tstatic float ShapeDensity( FillShape shape, float u, bool last ) => shape switch\n\t{\n\t\tFillShape.Ramp => 0.45f + 1.1f * u,\n\t\tFillShape.Pickup => last ? 1f : 0.22f,\n\t\tFillShape.Gesture => 0.15f + 0.35f * u,\n\t\t_ => 1f,\n\t};\n\n\t/// <summary>\n\t/// What the rest of the section is doing where this fill cell lands \u2014 the one thing a fill did\n\t/// not read, on a grid that was already built for it.\n\t///\n\t/// A fill is the drummer's bar, but it is not played over silence: the melodic voices play\n\t/// THROUGH a fill (only the kit hands over), so a fill that puts a hit on the note the tune is\n\t/// landing on is two things arriving on the same beat. And the seam is what the fill is FOR \u2014\n\t/// it is crossing one, and leaning on it is the gesture.\n\t///\n\t/// A multiplier on a probability, so it changes nothing about how much of the stream a fill\n\t/// spends: <see cref=\"FillCells\"/>'s rule holds, and the density target still means what it\n\t/// said. Deliberately gentle in both directions \u2014 a fill that dodged the tune outright would\n\t/// be a fill written by the melody.\n\t/// </summary>\n\tfloat FillAgainst( int tick )\n\t{\n\t\tvar sk = _skeleton;\n\t\tif ( sk == null ) return 1f;\n\t\tint c = sk.CellAt( tick );\n\t\tif ( c < 0 ) return 1f;\n\t\treturn (sk.TuneOn[c] ? 0.6f : 1f) * (sk.Seam[c] ? 1.25f : 1f);\n\t}\n\n\t// One fill across a span. The span is whatever FillStart drew, so the same code plays a\n\t// one-beat pickup and a two-bar blow-out; the terminal crash lands on the downbeat it is\n\t// leading into.\n\tvoid RenderFill( int fromTick, int toTick, Rng noise, Rng rng )\n\t{\n\t\tint span = toTick - fromTick;\n\t\tif ( span <= 0 ) return;\n\t\tint beats = Math.Max( 1, span / Timing.TicksPerBeat );\n\n\t\tvar shape = rng.PickWeighted( FillShapeTable, _prof.FillShapes );\n\t\t// A SHUFFLE IS ALREADY A TRIPLET FEEL, so a fill on the straight grid under one is not\n\t\t// straight \u2014 it is neither. Ticks are metrical and the shuffle is a warp applied on the way\n\t\t// to samples (see Timing), which interpolates between eighth ANCHORS: the four sixteenths of\n\t\t// a beat come out 2:2:1:1, so the back half of every beat runs at double the speed of the\n\t\t// front. At 115 bpm with swing 0.33 they land at 0/173/347/434 ms. That is right for a comp\n\t\t// landing an occasional sixteenth between two eighths the band shares, and wrong for the one\n\t\t// voice that runs continuous sixteenths \u2014 a drummer shuffling fills in triplets.\n\t\t//\n\t\t// The Chance draw still happens either way, so the genre's stream position is untouched: the\n\t\t// feel decides the GRID, never how much of the stream a fill spends (see FillCells).\n\t\tbool triplet = rng.Chance( _c.TripletChance ) || _time.Swing >= GenreProfile.ShuffleGrid;\n\t\tbool tomLed = rng.Chance( 0.45f );\n\n\t\t// A fill longer than a bar still has to keep the time while it happens. A gesture or a\n\t\t// pickup stretched over two bars is not a sparser fill, it is a hole in the arrangement \u2014\n\t\t// the kit has already handed over to it, so there is nothing else playing the beat.\n\t\tif ( beats > 4 && shape != FillShape.Rolling ) shape = FillShape.Ramp;\n\n\t\t// And the same rule from the other end: a PICKUP is a wait and then a flurry, so it needs a\n\t\t// span to wait in. Over one beat there is nothing to wait through and the shape degenerates\n\t\t// into the flurry alone \u2014 five or six 32nds in the beat the kit has just handed over to,\n\t\t// with no groove either side of them. That is the most common fill length there is (a beat\n\t\t// is 55% of the draw), so a genre with any weight on Pickup plays it constantly, and it\n\t\t// reads as a drummer arriving late and cramming the whole fill in anyway. A fill that short\n\t\t// accelerates into the bar line instead.\n\t\tif ( beats == 1 && shape == FillShape.Pickup ) shape = FillShape.Ramp;\n\n\t\tfloat[] grid = triplet ? FillTriplet : FillStraight;\n\t\t// The genre's hits-per-bar turned into per-cell probabilities: the position weights are the\n\t\t// SHAPE of a bar's occupancy and this fills them until they sum to the target. The flurry\n\t\t// keeps the flat scale instead, because being denser than an ordinary beat is what a flurry\n\t\t// IS \u2014 water-filling it to the same target would take the acceleration back out of it.\n\t\tfloat[] cells = FillChances( _prof.FillHits, grid );\n\t\tfloat flurryK = FillCellK( _prof.FillHits, grid );\n\n\t\tfor ( int b = 0; b < beats; b++ )\n\t\t{\n\t\t\tint beatTick = fromTick + b * Timing.TicksPerBeat;\n\t\t\tbool last = b == beats - 1;\n\t\t\tfloat dens = ShapeDensity( shape, beats == 1 ? 1f : b / (beats - 1f), last );\n\t\t\t// The flurry is a straight-grid gesture: a triplet fill is already a different feel\n\t\t\t// and does not need a second one laid over its last beat.\n\t\t\tbool flurry = shape == FillShape.Pickup && last && !triplet;\n\t\t\tint n = flurry ? FillFlurry.Length : cells.Length;\n\n\t\t\tfor ( int i = 0; i < FillCells; i++ )\n\t\t\t{\n\t\t\t\t// Both draws happen for every cell of every grid \u2014 see FillCells.\n\t\t\t\tfloat r = rng.Next(), d = rng.Next();\n\t\t\t\tif ( i >= n ) continue;\n\t\t\t\tfloat p = flurry ? flurryK * FillFlurry[i] : cells[i];\n\t\t\t\tint cellTick = beatTick + i * Timing.TicksPerBeat / n;\n\t\t\t\tif ( r >= Math.Clamp( p * dens * FillAgainst( cellTick ), 0f, 1f ) ) continue;\n\n\t\t\t\t// A tuplet divides its own span evenly; a straight cell is a grid position and\n\t\t\t\t// shuffles with everything else the band lands on (see Timing).\n\t\t\t\tint t = triplet\n\t\t\t\t\t? _time.EvenSpan( beatTick, Timing.TicksPerBeat, i / (double)n )\n\t\t\t\t\t: _time.TickToSample( cellTick );\n\n\t\t\t\t// A fill is a phrase: it leans into the bar line it is landing on, and it reads\n\t\t\t\t// the genre's accent weights like every other voice in the song.\n\t\t\t\tfloat u = (b + i / (float)n) / beats;\n\t\t\t\tfloat gain = KitGain( cellTick, 0.72f + 0.33f * u, 0.35f );\n\n\t\t\t\t// A fill goes round the kit high to low, which is what a three-piece tom set is\n\t\t\t\t// laid out for \u2014 and it goes there BY INDEX, so a fill cannot reach past the\n\t\t\t\t// bottom of the kit. It used to sweep six frequencies through a pan map that\n\t\t\t\t// bottomed out at 145 Hz, so the two lowest drums of every fill shared a position.\n\t\t\t\t// Snare-led unless the fill is a tom figure; the ride comes in where DrumTone leans\n\t\t\t\t// high, and the kick is under it either way \u2014 \"13.2 hits per bar\" is the whole kit,\n\t\t\t\t// and a drummer's foot is part of the kit.\n\t\t\t\tfloat tomShare = shape == FillShape.Gesture || tomLed ? 0.55f : 0.26f;\n\t\t\t\tfloat rideShare = 0.14f * _drumTone;\n\t\t\t\tif ( d < tomShare )\n\t\t\t\t\tRenderTom( t, _tomKit, Math.Min( TomKit.Count - 1, (int)(u * TomKit.Count) ),\n\t\t\t\t\t\tnoise, gain, TomTone.Default );\n\t\t\t\telse if ( d < tomShare + rideShare )\n\t\t\t\t\tRenderRideCym( t, _c.HatVol * gain, _rideBow );\n\t\t\t\telse if ( d < tomShare + rideShare + 0.08f )\n\t\t\t\t\tRenderKick( t, noise, gain, _kickTone, 0f );\n\t\t\t\telse RenderSnare( t, noise, false, gain );\n\t\t\t}\n\t\t}\n\t\t// The fill lands on the downbeat it was leading into, and it is a crash at a LEVEL now:\n\t\t// the voice had no gain parameter at all, so the loudest thing in a song arrived at full\n\t\t// scale whatever the section's energy, the velocity or the genre's accent said.\n\t\tbool darkCrash = rng.Chance( 0.4f );\n\t\tRenderCrashCym( _time.TickToSample( toTick ), _c.CrashVol * KitGain( toTick, 1f, 0.35f ),\n\t\t\tdarkCrash ? _crashDark : _crashBright, darkCrash );\n\t}\n}\n"
        },
        {
            "Ident": "gamah.skafinity",
            "Path": "Code/Engine/Master.cs",
            "FileName": "Master.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 341414,
            "IsPrivate": false,
            "Code": "using System;\nusing System.Collections.Generic;\n\nnamespace Skafinity;\n\n// Master bus: the reverb, then soft-clip and normalize.\n//\n// Part of the MusicGen engine \u2014 see MusicGen.cs.\n\npublic sealed partial class MusicGen\n{\n\t// Master: gentle soft-clip + normalize. The mix peak is first normalized to 1.0\n\t// BEFORE the soft-clipper so it always has headroom \u2014 otherwise a hot sustained\n\t// bed (all voices flat at 1.0) saturated the tanh and swallowed the drum\n\t// transients (kick/snare washed out). MasterDrive now sets how hard a\n\t// peak-normalized signal hits the clipper, so the dynamics stay intact.\n\t// Call only after every RenderPitchedRange window has completed.\n\tfloat Master()\n\t{\n\t\tint total = _bufL.Length;\n\t\tfloat rawPeak = 0f;\n\t\tfor ( int i = 0; i < total; i++ )\n\t\t\trawPeak = Math.Max( rawPeak, Math.Max( MathF.Abs( _bufL[i] ), MathF.Abs( _bufR[i] ) ) );\n\t\tfloat pre = rawPeak > 0.0001f ? _c.MasterDrive / rawPeak : _c.MasterDrive;\n\n\t\tfor ( int i = 0; i < total; i++ )\n\t\t{\n\t\t\t_bufL[i] = (float)Math.Tanh( _bufL[i] * pre );\n\t\t\t_bufR[i] = (float)Math.Tanh( _bufR[i] * pre );\n\t\t}\n\n\t\t// A touch of stereo room reverb \u2014 the dry mix alone read flat/\"16-bit\".\n\t\tApplyReverb();\n\n\t\tfloat peak = 0f;\n\t\tfor ( int i = 0; i < total; i++ )\n\t\t{\n\t\t\tfloat a = Math.Max( MathF.Abs( _bufL[i] ), MathF.Abs( _bufR[i] ) );\n\t\t\tif ( a > peak ) peak = a;\n\t\t}\n\t\treturn peak > 0.0001f ? _c.MasterPeak / peak : 1f;\n\t}\n\n\t// The song's room, from the two Config knobs that shape it \u2014 trimmed by the genre's own mix\n\t// profile. A room is part of what a genre sounds like: ska is recorded in one, metal is\n\t// recorded dry and close, country sits in between and centred. The REVERB knob still rides on\n\t// top; the trim moves what \"0.5\" means for this genre.\n\tvoid ApplyReverb()\n\t{\n\t\tfloat wet = Math.Clamp( _reverbWet * _c.MasterReverb * MixTrim( _prof.Mix.Reverb ), 0f, 1f );\n\t\tif ( wet <= 0.0001f ) return;\n\t\t// Tail length. The top of this used to be 0.98, which over combs 25\u201334 ms long is a ~9\n\t\t// second decay \u2014 long past a room and into a delay line whose repeats smear every chord\n\t\t// into the next. 0.94 is ~3 s, which is the longest tail that is still a SPACE the band is\n\t\t// playing in. The tail also darkens as it lengthens: a bright decay that outlasts the bar\n\t\t// accumulates top end instead of dying, and that ringing IS the muddiness.\n\t\tfloat decay = Math.Clamp( _c.ReverbDecay, 0f, 1f );\n\t\tfloat feedback = 0.70f + 0.24f * decay;\n\t\tfloat damp = 0.25f + 0.35f * decay;\n\t\tReverb.Process( _bufL, 0, wet, feedback, damp, _sr );\n\t\tReverb.Process( _bufR, Reverb.StereoSpread, wet, feedback, damp, _sr );\n\t}\n}\n"
        },
        {
            "Ident": "gamah.skafinity",
            "Path": "Code/Engine/Wav.cs",
            "FileName": "Wav.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 341414,
            "IsPrivate": false,
            "Code": "using System;\nusing System.Collections.Generic;\n\nnamespace Skafinity;\n\n/// <summary>\n/// The WAV container \u2014 16-bit PCM, the one format both targets can hand straight to a player.\n/// Stateless: it wraps samples somebody else rendered.\n/// </summary>\nstatic class Wav\n{\n\t/// <summary>Clamp a \u22121..1 mix sample to signed 16-bit.</summary>\n\tpublic static short ToS16( float v ) => (short)(Math.Clamp( v, -1f, 1f ) * 32767f);\n\n\t/// <summary>Wrap already-rendered 16-bit samples in a WAV. Mono or interleaved stereo per\n\t/// <paramref name=\"channels\"/>.</summary>\n\tpublic static byte[] FromSamples( short[] samples, int channels, int sampleRate )\n\t{\n\t\tint dataSize = samples.Length * 2;\n\t\tint blockAlign = channels * 2;\n\t\tvar bytes = new List<byte>( 44 + dataSize );\n\t\tvoid Str( string s ) { foreach ( var ch in s ) bytes.Add( (byte)ch ); }\n\t\tvoid U32( uint v ) { bytes.Add( (byte)v ); bytes.Add( (byte)(v >> 8) ); bytes.Add( (byte)(v >> 16) ); bytes.Add( (byte)(v >> 24) ); }\n\t\tvoid U16( ushort v ) { bytes.Add( (byte)v ); bytes.Add( (byte)(v >> 8) ); }\n\t\tStr( \"RIFF\" ); U32( (uint)(36 + dataSize) ); Str( \"WAVE\" );\n\t\tStr( \"fmt \" ); U32( 16 ); U16( 1 ); U16( (ushort)channels );\n\t\tU32( (uint)sampleRate ); U32( (uint)(sampleRate * blockAlign) ); U16( (ushort)blockAlign ); U16( 16 );\n\t\tStr( \"data\" ); U32( (uint)dataSize );\n\t\tforeach ( var s in samples ) { ushort u = (ushort)s; bytes.Add( (byte)u ); bytes.Add( (byte)(u >> 8) ); }\n\t\treturn bytes.ToArray();\n\t}\n}\n\npublic sealed partial class MusicGen\n{\n\t// \u2500\u2500 Output \u2500\u2500\n\tshort[] ToShorts( float gain )\n\t{\n\t\tint n = _bufL.Length;\n\t\tvar s = new short[n * Channels];\n\t\tfor ( int i = 0; i < n; i++ )\n\t\t{\n\t\t\ts[i * 2] = Wav.ToS16( _bufL[i] * gain );\n\t\t\ts[i * 2 + 1] = Wav.ToS16( _bufR[i] * gain );\n\t\t}\n\t\treturn s;\n\t}\n\n\t/// <summary>Wrap already-rendered 16-bit samples in a WAV (for export). Mono or\n\t/// interleaved stereo per <paramref name=\"channels\"/>.</summary>\n\tpublic static byte[] WavFromSamples( short[] samples, int channels, int sampleRate )\n\t\t=> Wav.FromSamples( samples, channels, sampleRate );\n\n\tbyte[] EncodeWav( float gain ) => Wav.FromSamples( ToShorts( gain ), Channels, _sr );\n}\n"
        },
        {
            "Ident": "gamah.skafinity",
            "Path": "Code/SkafinityCommands.cs",
            "FileName": "SkafinityCommands.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 341414,
            "IsPrivate": false,
            "Code": "using System.Linq;\nusing Sandbox;\n\nnamespace Skafinity;\n\n/// <summary>\n/// Console commands for driving the player and the panel from inside the editor.\n///\n/// <para>They exist because the two things a host most needs to try are the two things it cannot\n/// reach without writing code first. The board <b>ships no launcher</b> \u2014 visibility is\n/// host-driven on purpose, so it imposes nothing on your HUD \u2014 which means a freshly-dropped\n/// <see cref=\"SkafinityMusicPanel\"/> renders nothing at all until you have bound\n/// <see cref=\"SkafinityMusicPanel.IsOpen\"/> to something. And <see cref=\"SkafinityTheme.Accent\"/>\n/// is a static a game sets at startup, so seeing what your colour looks like used to mean a\n/// rebuild per guess. <c>skafinity_panel</c> and <c>skafinity_theme</c> are those two, live.</para>\n///\n/// <para>The rest are the seed: play one, step it, switch genre, reroll, and read back either the\n/// player's state or what the composer actually decided.</para>\n///\n/// <para>s&amp;box-only (outside <c>Code/Engine/</c>), and client-side \u2014 the player is client-only\n/// (<c>DontExecuteOnServer</c>), so these are too.</para>\n/// </summary>\npublic static class SkafinityCommands\n{\n\t/// <summary>Name of the GameObject <see cref=\"Spawn\"/> builds. Also how <see cref=\"Despawn\"/>\n\t/// finds it again, so it only ever destroys its own rig and never a scene-authored player.</summary>\n\tconst string RigName = \"Skafinity (console)\";\n\n\t// Every command needs the player, and \"there isn't one\" is the single most likely reason a\n\t// command does nothing \u2014 so say so rather than failing silently.\n\tstatic SkafinityPlayer Player()\n\t{\n\t\tvar scene = Sandbox.Game.ActiveScene;\n\t\tif ( scene == null ) { Log.Warning( \"[Skafinity] no active scene.\" ); return null; }\n\n\t\tvar p = scene.GetAllComponents<SkafinityPlayer>().FirstOrDefault();\n\t\tif ( p == null )\n\t\t\tLog.Warning( \"[Skafinity] no SkafinityPlayer in the scene \u2014 run skafinity_spawn for a \"\n\t\t\t\t+ \"throwaway one, or add the component to a GameObject yourself.\" );\n\t\treturn p;\n\t}\n\n\t/// <summary>Build a throwaway board (and a player, if the scene hasn't got one) on a runtime\n\t/// GameObject, so the library can be tried in ANY scene without one being authored for it.\n\t/// <c>skafinity_despawn</c> removes it.</summary>\n\t/// <remarks>It never makes a SECOND of anything: a board already in the scene \u2014 a previous rig\n\t/// or the game's own UI \u2014 is the one it hands back, and a player already in the scene is the\n\t/// one the board drives. So this is safe to run in a game that has Skafinity wired up properly,\n\t/// where it does nothing but tell you so.</remarks>\n\t/// <remarks>Client-local and never saved: <c>NetworkMode.Never</c> so it is not replicated,\n\t/// <c>GameObjectFlags.NotSaved</c> so it cannot end up committed in someone's scene file. It\n\t/// carries its OWN <c>ScreenPanel</c> rather than hunting for the scene's, which is what makes\n\t/// it work in a scene that has no UI root at all. Shape copied from rotaliate-client's\n\t/// <c>LocalMusicSystem</c>, which builds the same three components for real.</remarks>\n\t[ConCmd( \"skafinity_spawn\" )]\n\tpublic static void Spawn()\n\t{\n\t\tvar panel = BuildRig( out bool created );\n\t\tif ( panel == null ) return;\n\n\t\tLog.Info( created\n\t\t\t? $\"[Skafinity] rig ready \u2014 '{RigName}'. skafinity_panel opens it, skafinity_despawn removes it.\"\n\t\t\t: $\"[Skafinity] nothing spawned \u2014 this scene already has a board, on '{panel.GameObject?.Name}'. \"\n\t\t\t  + \"skafinity_panel opens it.\" );\n\t}\n\n\t/// <summary>Destroy the rig <c>skafinity_spawn</c> built. Leaves a scene-authored player alone \u2014\n\t/// it only removes the GameObject it made.</summary>\n\t[ConCmd( \"skafinity_despawn\" )]\n\tpublic static void Despawn()\n\t{\n\t\tvar scene = Sandbox.Game.ActiveScene;\n\t\tif ( scene == null ) { Log.Warning( \"[Skafinity] no active scene.\" ); return; }\n\n\t\tvar rig = FindRig( scene );\n\t\tif ( rig == null ) { Log.Info( \"[Skafinity] no console rig to remove.\" ); return; }\n\n\t\trig.Destroy();\n\t\tLog.Info( $\"[Skafinity] removed '{RigName}'.\" );\n\t}\n\n\t/// <summary>Open/close the settings board \u2014 the panel ships no launcher of its own, so this is\n\t/// how you see it at all. Builds a rig first if the scene has no board (see\n\t/// <see cref=\"Spawn\"/>), so this one command works in an empty scene.</summary>\n\t[ConCmd( \"skafinity_panel\" )]\n\tpublic static void TogglePanel()\n\t{\n\t\t// Nothing to toggle rather than nothing to do: the point of the command is to see the board,\n\t\t// and needing a scene authored first is the whole problem it exists to solve. BuildRig\n\t\t// returns the scene's own board where there is one, so this never makes a second.\n\t\tvar panel = BuildRig( out bool created );\n\t\tif ( panel == null ) return;\n\t\tif ( created )\n\t\t\tLog.Info( $\"[Skafinity] no board in the scene \u2014 built '{RigName}'. skafinity_despawn removes it.\" );\n\n\t\tpanel.Toggle();\n\t\tLog.Info( $\"[Skafinity] board {( panel.IsOpen ? \"OPEN\" : \"closed\" )}.\" );\n\t}\n\n\t// The rig itself. Returns the board to drive \u2014 an existing one wherever there is one \u2014 or null\n\t// with a reason logged. `created` says whether anything was actually built.\n\tstatic SkafinityMusicPanel BuildRig( out bool created )\n\t{\n\t\tcreated = false;\n\t\tvar scene = Sandbox.Game.ActiveScene;\n\t\tif ( scene == null ) { Log.Warning( \"[Skafinity] no active scene.\" ); return null; }\n\n\t\t// A play-mode thing. In the editor's edit scene there is no audio to hear and no reason to\n\t\t// be adding objects to what someone is authoring, even unsaveable ones.\n\t\tif ( scene.IsEditor )\n\t\t{\n\t\t\tLog.Warning( \"[Skafinity] press play first \u2014 the rig is built into the running scene, not the edit scene.\" );\n\t\t\treturn null;\n\t\t}\n\n\t\t// A headless server has no audio device and no screen; the player is DontExecuteOnServer\n\t\t// for the same reason, so building it there would produce an inert object and a puzzle.\n\t\tif ( Application.IsDedicatedServer )\n\t\t{\n\t\t\tLog.Warning( \"[Skafinity] not on a dedicated server \u2014 Skafinity is client-side (audio + UI).\" );\n\t\t\treturn null;\n\t\t}\n\n\t\t// NEVER a second board. Any panel already in the scene is the one to drive, whether it is a\n\t\t// previous rig or one the game authored \u2014 two boards would be two sets of controls over one\n\t\t// player, and the second would be the game's own UI duplicated by a debug command.\n\t\tvar existing = scene.GetAllComponents<SkafinityMusicPanel>().FirstOrDefault();\n\t\tif ( existing != null ) return existing;\n\n\t\tcreated = true;\n\t\tvar go = new GameObject( true, RigName ) { Flags = GameObjectFlags.NotSaved };\n\t\tgo.NetworkMode = NetworkMode.Never;   // strictly local: this is a test rig, not game state\n\n\t\t// Its own UI root, so this works in a scene with no ScreenPanel of its own.\n\t\tgo.Components.Create<ScreenPanel>();\n\n\t\t// Reuse a player the scene already has rather than starting a second soundtrack over it.\n\t\tvar player = scene.GetAllComponents<SkafinityPlayer>().FirstOrDefault()\n\t\t\t?? go.Components.Create<SkafinityPlayer>();\n\n\t\tvar panel = go.Components.Create<SkafinityMusicPanel>();\n\t\tpanel.Player = player;\n\t\treturn panel;\n\t}\n\n\t// Only ever OUR GameObject: matched by name, so a scene-authored player is never a candidate.\n\tstatic GameObject FindRig( Scene scene )\n\t{\n\t\tforeach ( var p in scene.GetAllComponents<SkafinityMusicPanel>() )\n\t\t\tif ( p.GameObject != null && p.GameObject.Name == RigName )\n\t\t\t\treturn p.GameObject;\n\t\treturn null;\n\t}\n\n\t/// <summary>Retint the board from one colour: <c>skafinity_theme #ff8a3d</c>. Pass\n\t/// <c>clear</c> (or <c>none</c> / <c>neutral</c>) to go back to the neutral gray/black default.\n\t/// This is the whole of what a consuming game does \u2014 it sets\n\t/// <see cref=\"SkafinityTheme.Accent\"/> once \u2014 so what you see here is what you get by shipping\n\t/// that one line.</summary>\n\t[ConCmd( \"skafinity_theme\" )]\n\tpublic static void SetTheme( string accent )\n\t{\n\t\tif ( string.IsNullOrWhiteSpace( accent ) || accent is \"clear\" or \"none\" or \"neutral\" )\n\t\t{\n\t\t\tSkafinityTheme.Accent = null;\n\t\t\tLog.Info( \"[Skafinity] theme cleared \u2014 neutral gray/black (the library default).\" );\n\t\t\treturn;\n\t\t}\n\n\t\tvar c = Color.Parse( accent );\n\t\tif ( c == null )\n\t\t{\n\t\t\tLog.Warning( $\"[Skafinity] couldn't parse '{accent}' as a colour \u2014 try a hex like #2f9450.\" );\n\t\t\treturn;\n\t\t}\n\n\t\tSkafinityTheme.Accent = c;\n\t\tLog.Info( $\"[Skafinity] accent = {accent}. In your game: SkafinityTheme.Accent = Color.Parse( \\\"{accent}\\\" );\" );\n\t}\n\n\t/// <summary>Play a seed: <c>tag:n[:genre][:vibe]</c> (a bare <c>tag</c> is song 0). Pass\n\t/// <c>default</c> to go back to the default tag and vibe.</summary>\n\t[ConCmd( \"skafinity_seed\" )]\n\tpublic static void PlaySeed( string seed )\n\t{\n\t\tvar p = Player();\n\t\tif ( p == null ) return;\n\n\t\tif ( seed is \"default\" ) { p.SetTag( \"\" ); Log.Info( \"[Skafinity] back to the default tag and vibe.\" ); return; }\n\n\t\tp.PlaySeed( seed );\n\t\tLog.Info( $\"[Skafinity] playing {p.CurrentSeed}\" );\n\t}\n\n\t/// <summary>Next song in the sequence.</summary>\n\t[ConCmd( \"skafinity_next\" )]\n\tpublic static void Next() { var p = Player(); if ( p == null ) return; p.NextSong(); Log.Info( $\"[Skafinity] \u2192 {p.CurrentSeed}\" ); }\n\n\t/// <summary>Previous song \u2014 replays the exact earlier song, not a fresh one.</summary>\n\t[ConCmd( \"skafinity_prev\" )]\n\tpublic static void Prev() { var p = Player(); if ( p == null ) return; p.PrevSong(); Log.Info( $\"[Skafinity] \u2190 {p.CurrentSeed}\" ); }\n\n\t/// <summary>Switch genre by index. Run it with a junk index to print the roster.</summary>\n\t[ConCmd( \"skafinity_genre\" )]\n\tpublic static void SetGenre( int genre )\n\t{\n\t\tvar p = Player();\n\t\tif ( p == null ) return;\n\n\t\tif ( genre < 0 || genre >= VibeCodec.GenreCount )\n\t\t{\n\t\t\tLog.Warning( $\"[Skafinity] genre {genre} is out of range. {Roster()}\" );\n\t\t\treturn;\n\t\t}\n\n\t\tp.SetGenre( genre );\n\t\tLog.Info( $\"[Skafinity] genre {genre} = {VibeCodec.Genres[genre]} \u2014 {p.CurrentSeed}\" );\n\t}\n\n\t/// <summary>Reroll the vibe: every knob thrown somewhere new and PINNED there, keeping the genre\n\t/// and your per-instrument volumes. <c>skafinity_station</c> is the other die \u2014 a different song\n\t/// rather than a different taste.</summary>\n\t[ConCmd( \"skafinity_reroll\" )]\n\tpublic static void Reroll()\n\t{\n\t\tvar p = Player();\n\t\tif ( p == null ) return;\n\n\t\tp.RerollVibe();\n\t\tLog.Info( $\"[Skafinity] rerolled \u2014 {p.CurrentSeed}\" );\n\t}\n\n\t/// <summary>A fresh random station at song 0. Anything pinned stays pinned.</summary>\n\t[ConCmd( \"skafinity_station\" )]\n\tpublic static void Station()\n\t{\n\t\tvar p = Player();\n\t\tif ( p == null ) return;\n\n\t\tp.RerollStation();\n\t\tLog.Info( $\"[Skafinity] new station \u2014 {p.StationSeed}\" );\n\t}\n\n\t/// <summary>Flip shuffle: on, every next song is a whole new station rather than the next song of\n\t/// this one.</summary>\n\t[ConCmd( \"skafinity_shuffle\" )]\n\tpublic static void ToggleShuffle()\n\t{\n\t\tvar p = Player();\n\t\tif ( p == null ) return;\n\n\t\tp.SetShuffle( !p.Shuffle );\n\t\tLog.Info( $\"[Skafinity] shuffle {( p.Shuffle ? \"on\" : \"off\" )} \u2014 {p.StationSeed}\" );\n\t}\n\n\t/// <summary>Pause or resume, keeping the place in the song.</summary>\n\t[ConCmd( \"skafinity_pause\" )]\n\tpublic static void TogglePause()\n\t{\n\t\tvar p = Player();\n\t\tif ( p == null ) return;\n\n\t\tp.TogglePlay();\n\t\tvar at = p.Playhead();\n\t\tLog.Info( $\"[Skafinity] {( p.IsPaused ? \"paused\" : \"playing\" )} at {SkafinityBoard.Time( at.Time, at.Duration > 0 )}\" );\n\t}\n\n\t/// <summary>Write the playing song to a .wav under the s&amp;box data folder.</summary>\n\t[ConCmd( \"skafinity_save\" )]\n\tpublic static void Save()\n\t{\n\t\tvar p = Player();\n\t\tif ( p == null ) return;\n\n\t\tvar name = p.SaveCurrentToFile();\n\t\tLog.Info( string.IsNullOrEmpty( name )\n\t\t\t? \"[Skafinity] couldn't save \u2014 nothing rendered yet?\"\n\t\t\t: $\"[Skafinity] saved {name} to your s&box data folder.\" );\n\t}\n\n\t/// <summary>What the player is doing right now: the seed, the transport, and whether the\n\t/// shared house mix actually loaded.</summary>\n\t[ConCmd( \"skafinity_status\" )]\n\tpublic static void Status()\n\t{\n\t\tvar p = Player();\n\t\tif ( p == null ) return;\n\n\t\tint genre = p.EffectiveConfig()?.Genre ?? 0;\n\t\tvar at = p.Playhead();\n\t\tLog.Info( \"\u2500\u2500 skafinity_status \u2500\u2500\" );\n\t\tLog.Info( $\"   seed      {p.CurrentSeed}   (n {p.N}, genre {genre} = {VibeCodec.Genres[genre]})\" );\n\t\tLog.Info( $\"   station   {p.StationSeed}   (position {p.Position} on the line)\" );\n\t\tLog.Info( $\"   transport {( p.Enabled ? \"on\" : \"MUTED\" )}, vol {p.Volume:0.00}, \"\n\t\t\t+ $\"{( p.IsPaused ? \"paused\" : p.IsPlaying ? \"playing\" : \"not playing\" )} \"\n\t\t\t+ $\"{SkafinityBoard.Time( at.Time, at.Duration > 0 )} / {SkafinityBoard.Time( at.Duration, at.Duration > 0 )}\"\n\t\t\t+ $\"{( p.IsBuffering ? \", BUFFERING\" : p.IsGenerating ? \", generating ahead\" : \"\" )}\" );\n\t\tLog.Info( $\"   rolling   genre {( p.GenrePinned ? \"pinned\" : \"per song\" )}, \"\n\t\t\t+ $\"vibe {( p.VibePinned ? \"pinned\" : \"per song\" )}\" );\n\t\tLog.Info( $\"   shuffle   {( p.Shuffle ? \"on \u2014 every next song is a new station\" : \"off \u2014 walking this station\" )}\" );\n\t\tLog.Info( $\"   output    {p.SampleRate} Hz, {p.RenderThreads} render thread(s)\" );\n\t\t// Zero here is the interesting case: the baseline mix is then the engine's compiled\n\t\t// defaults, not the file the web toy reads, and nothing else would ever say so.\n\t\tLog.Info( p.HouseConfigCount > 0\n\t\t\t? $\"   housemix  {p.HouseConfigCount} values from skafinity.config.json\"\n\t\t\t: \"   housemix  NOT LOADED \u2014 skafinity.config.json isn't mounted, so the baseline mix is \"\n\t\t\t  + \"the compiled defaults rather than the shared file. Check it shipped with the addon.\" );\n\t\tLog.Info( $\"   theme     {( SkafinityTheme.Accent == null ? \"neutral (accent unset)\" : SkafinityTheme.Accent.ToString() )}\" );\n\t\tLog.Info( $\"   {Roster()}\" );\n\t}\n\n\t/// <summary>What the composer decided for the song playing: tempo, swing, key, changes,\n\t/// voicing, groove, part and tune lengths, ending, and the form. This is the \"why does this\n\t/// seed sound wrong\" tool \u2014 reading the decisions beats inferring them from the audio.\n\t/// Re-plans the song, so expect a short hitch.</summary>\n\t[ConCmd( \"skafinity_explain\" )]\n\tpublic static void Explain()\n\t{\n\t\tvar p = Player();\n\t\tif ( p == null ) return;\n\n\t\tLog.Info( $\"\u2500\u2500 skafinity_explain {p.CurrentSeed} \u2500\u2500\" );\n\t\tLog.Info( p.ExplainCurrent() );\n\t}\n\n\tstatic string Roster() =>\n\t\t\"genres: \" + string.Join( \"  \", VibeCodec.Genres.Select( ( g, i ) => $\"{i}={g}\" ) );\n}\n"
        },
        {
            "Ident": "gamah.skafinity",
            "Path": "Engine/Harmony.cs",
            "FileName": "Harmony.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 341414,
            "IsPrivate": false,
            "Code": "using System;\nusing System.Collections.Generic;\n\nnamespace Skafinity;\n\n/// <summary>\n/// Harmony: the per-genre scale / progression / bass-pattern tables, and the degree\u2192pitch\n/// maths that reads them.\n///\n/// A progression entry is a SCALE DEGREE, not a semitone \u2014 so the same progression table\n/// reads as major or minor depending on the scale drawn alongside it, and a degree of 5\n/// against a minor scale is a \u266dVI. Degrees are unbounded: <see cref=\"ScaleMidi\"/> wraps\n/// octaves rather than clamping, so running off either end of a scale still lands on a sane\n/// pitch. That is what lets a progression be any length \u2014 nothing here assumes four.\n///\n/// Stateless by design: every entry point takes the scale it should read. MusicGen keeps thin\n/// instance wrappers that supply the song's own scale. Which table a genre draws from is\n/// <see cref=\"GenreProfile\"/>'s business, not this file's \u2014 these are just the tables.\n///\n/// NO TWO GENRES SHARE MORE THAN ONE PROGRESSION, OR MORE THAN ONE SCALE. Sharing them is how\n/// six genres came to draw byte-identical changes (I\u2013V\u2013vi\u2013IV was in four of them; major was in\n/// four scale tables), so both sets of tables are pruned to keep the genres apart and the engine\n/// test asserts the cap on each. Adding an entry means checking it against the other five.\n///\n/// WEIGHTS ARE REAL WEIGHTS. The tables used to bias a draw by listing an entry twice, which\n/// silently made \"how likely\" and \"how many entries\" the same knob \u2014 a genre could not lean on\n/// its home mode without also diluting the overlap cap. Each table now carries a parallel weight\n/// array (see <see cref=\"GenreProfile\"/>), drawn with one <c>rng.Next()</c> exactly like the old\n/// <c>Pick</c>, so a genre's draw count never depends on how it is weighted.\n/// </summary>\nstatic class Harmony\n{\n\t/// <summary>Bass-pattern cell: no onset (the previous note sustains).</summary>\n\tpublic const int Rest = -99;\n\n\t/// <summary>Bass-pattern cell: walk into the next chord instead of playing a fixed\n\t/// offset.</summary>\n\tpublic const int Approach = 99;\n\n\t// \u2500\u2500 Chord voicings \u2500\u2500\n\t// Offsets in SCALE-DEGREE space from the chord's own degree, so a voicing follows the mode\n\t// the way the rest of the engine does: {0,2,4} is a diatonic triad, {0,2,4,6} adds the 7th,\n\t// {0,4} is the bare power chord (root + 5th). Nothing in the engine played anything but a\n\t// triad or a power chord, which is why every genre's harmony read as the same primary-colour\n\t// chord set even under different roots. The chordal voices draw from their genre's table.\n\t/// <summary>Degree offsets inside a voicing. The THIRD decides major or minor and is the note a\n\t/// driven guitar leaves out; the FOURTH and the FIFTH are the ones that must stay PERFECT (see\n\t/// MusicGen.VoicedTone), because they are what \"sus4\" and \"power chord\" mean.</summary>\n\tpublic const int Third = 2, Fourth = 3, Fifth = 4;\n\n\t/// <summary>The SECOND \u2014 the other degree a suspension puts where the third belongs.</summary>\n\tpublic const int Second = 1;\n\n\t/// <summary>Index of the voice a suspension occupies in <paramref name=\"voicing\"/>, or -1 if it\n\t/// is not suspended.\n\t///\n\t/// A SUSPENSION IS A DELAYED THIRD, NOT A CHORD QUALITY. sus4 and sus2 put the fourth or the\n\t/// second in the third's place, so a chord voiced that way states no quality \u2014 and the song's\n\t/// voicing is drawn once, for every chordal voice and every chord. Held that way for a whole\n\t/// song nothing is out of key and every voice agrees; the song simply has no major and no\n\t/// minor, and an ear with nothing to resolve to hears the ambiguity as dissonance. The\n\t/// suspended note has to arrive somewhere, so <see cref=\"MusicGen.VoicingAt\"/> hands the\n\t/// chordal voices the resolved spelling over the back half of every chord's span: the chord\n\t/// hangs, then it lands.\n\t///\n\t/// A voicing that already contains the third is not suspended \u2014 the sixth's added 6th and the\n\t/// add9's 9th are colour over a stated triad, not a substitution. Neither is the power chord:\n\t/// it OMITS the third rather than replacing it, which is a sound in its own right (it is what\n\t/// a driven guitar plays) and there is nothing owed.</summary>\n\tpublic static int SuspendedVoice( int[] voicing )\n\t{\n\t\tint sus = -1;\n\t\tfor ( int i = 0; i < voicing.Length; i++ )\n\t\t{\n\t\t\tif ( voicing[i] == Third ) return -1;\n\t\t\tif ( voicing[i] == Second || voicing[i] == Fourth ) sus = i;\n\t\t}\n\t\treturn sus;\n\t}\n\n\tpublic static readonly int[] Triad = { 0, 2, 4 };\n\tpublic static readonly int[] Seventh = { 0, 2, 4, 6 };\n\tpublic static readonly int[] Ninth = { 0, 2, 4, 6, 8 };\n\tpublic static readonly int[] Sixth = { 0, 2, 4, 5 };\n\tpublic static readonly int[] Sus4 = { 0, 3, 4 };\n\tpublic static readonly int[] Sus2 = { 0, 1, 4 };\n\tpublic static readonly int[] Add9 = { 0, 2, 4, 8 };\n\tpublic static readonly int[] Power = { 0, 4 };\n\tpublic static readonly int[] PowerFlat7 = { 0, 4, 6 };\n\n\t// \u2500\u2500 Ska-punk harmony (Genre 0) \u2500\u2500\n\t// Bright and major: third-wave ska is upbeat major-key music, and mixolydian is what keeps its\n\t// \u266dVII moves available. The wave shift moved the WEIGHT toward plain major rather than the\n\t// table itself \u2014 the modes were already right, which is the part of the genre that did not\n\t// need retuning.\n\tpublic static readonly int[][] SkaPunkScales =\n\t{\n\t\tnew[] { 0, 2, 4, 5, 7, 9, 11 }, // major\n\t\tnew[] { 0, 2, 4, 5, 7, 9, 10 }, // mixolydian\n\t};\n\tpublic static readonly int[] SkaPunkScaleWeights = { 4, 2 };\n\n\t// The 7ths and 9ths stay: they are what make the CLEAN SKANK read as ska rather than as a\n\t// bright rock stab, and the skank is still what the verses play. They cost nothing in the loud\n\t// sections \u2014 the driven guitar drops its third there anyway (see DrivenVoicing), so the same\n\t// voicing spells a rocksteady chop in the verse and a power chord in the chorus.\n\tpublic static readonly int[][] SkaPunkVoicings = { Seventh, Ninth, Triad, Sixth };\n\tpublic static readonly int[] SkaPunkVoicingWeights = { 3, 2, 3, 1 };\n\n\t// The bright turnarounds ska keeps. The mixolydian \u266dVII moves are the ones doing the work\n\t// here: punk and pop hold the plain-major anthem loops (I\u2013V\u2013vi\u2013IV, vi\u2013IV\u2013I\u2013V, I\u2013IV\u2013V\u2013V,\n\t// I\u2013V\u2013IV\u2013V), and ska-punk sits close enough to punk that \u266dVII is most of what is left to tell\n\t// them apart harmonically \u2014 so nothing here may drift toward those tables. The 50s/rocksteady\n\t// ii\u2013V turnaround went with the wave: it is the sound of the era this genre no longer is.\n\tpublic static readonly int[][] SkaPunkProgressions =\n\t{\n\t\tnew[] { 0, 3, 4, 3 }, // I\u2013IV\u2013V\u2013IV\n\t\tnew[] { 0, 6, 3, 0 }, // I\u2013\u266dVII\u2013IV\u2013I (mixolydian)\n\t\tnew[] { 0, 6, 4, 0 }, // I\u2013\u266dVII\u2013V\u2013I (the mixolydian cadence)\n\t\tnew[] { 0, 3, 0, 4 }, // I\u2013IV\u2013I\u2013V\n\t\tnew[] { 5, 4, 3, 4 }, // vi\u2013V\u2013IV\u2013V (the minor-tinged vamp)\n\t\tnew[] { 0, 0, 3, 4 }, // I pedal \u2192 IV\u2013V\n\t};\n\n\t// \u2500\u2500 Bass pattern libraries \u2500\u2500\n\t// Cells are semitone offsets from the chord root, one per eighth; Rest carries no onset (the\n\t// previous note sustains through it) and Approach walks into the next chord.\n\t//\n\t// These are Patterns, not int[8]: a pattern owns its LENGTH, so a genre's line can be a\n\t// two-bar phrase that answers itself or a four-bar one that varies its last bar, instead of\n\t// one bar repeated until the section ends. That is also what keeps the libraries apart \u2014 the\n\t// old tables shared literal rows ({0,0,0,0,0,0,0,App} was in four of the five).\n\tstatic Pattern P( params int[] cells ) => Pattern.Eighths( cells );\n\n\t// Ska-punk: a DRIVING bass. The spacious one-drop and the long legato 1\u21945 lines that\n\t// used to live here are rocksteady/reggae playing \u2014 the right part for a genre this engine does\n\t// not have yet (see PLAN.md), and recoverable from git history when it does. A ska-punk bassist\n\t// runs eighths under the skank and pops the octave, closer to punk than to reggae; what keeps\n\t// these apart from PunkBass is that they still MOVE \u2014 walking lines and octave answers rather\n\t// than the undifferentiated chug that is punk's whole idea.\n\tpublic static readonly Pattern[] SkaPunkBass =\n\t{\n\t\tP( 0, 0, 0, 0, 7, 0, 0, 0,\n\t\t   0, 0, 0, 0, 12, 0, 7, Approach ),                              // driving eighths, octave on the way out (2 bars)\n\t\tP( 0, 12, 0, 12, 0, 12, 7, Approach ),                            // the octave pump (1 bar)\n\t\tP( 0, 0, 2, 0, 4, 0, 5, 0,\n\t\t   7, 0, 5, 0, 4, 0, 2, Approach ),                               // walking eighths up and back (2 bars)\n\t\tP( 0, Rest, 0, 7, Rest, 0, 12, Rest,\n\t\t   0, Rest, 0, 7, Rest, 12, 7, Approach ),                        // the verse line that breathes under the skank (2 bars)\n\t\tP( 0, 0, 0, 0, 0, 0, 12, 0,\n\t\t   0, 0, 0, 0, 7, 0, 12, 0,\n\t\t   0, 0, 0, 0, 0, 0, 12, 0,\n\t\t   0, 7, 5, 4, 2, 0, 0, Approach ),                               // four bars that walk out of the phrase\n\t};\n\n\t// \u2500\u2500 Rock harmony (Genre 1) \u2500\u2500\n\t// Minor-3rd modes throughout: the RockProgressions are written as MINOR (i\u2013\u266dVII\u2013\u266dVI \u2026), so a\n\t// major-3rd mode would flip the tonic major and the dark rock vamp would evaporate. Phrygian\n\t// went to metal \u2014 rock and metal sharing three modes was how two genres in the same tonality\n\t// could draw the identical mode under their now-different changes.\n\tpublic static readonly int[][] RockScales =\n\t{\n\t\tnew[] { 0, 2, 3, 5, 7, 8, 10 }, // natural minor (aeolian)\n\t\tnew[] { 0, 2, 3, 5, 7, 9, 10 }, // dorian (minor, brighter \u266e6 \u2014 classic rock)\n\t};\n\tpublic static readonly int[] RockScaleWeights = { 3, 2 };\n\n\t// Rock voices the triad plainly and reaches for a suspension rather than an extension \u2014 the\n\t// sus4 that hangs and resolves is the rock chord move ska's 7ths and 9ths are not.\n\tpublic static readonly int[][] RockVoicings = { Triad, Sus4, Power };\n\tpublic static readonly int[] RockVoicingWeights = { 3, 2, 2 };\n\n\t// Degrees are read against the (often minor) scale, so 5 = \u266dVI, 6 = \u266dVII, 3 = iv, 4 = v,\n\t// 2 = \u266dIII. Rock and metal both live in minor, so they had three progressions in common and\n\t// could draw the identical vamp; the \u266dVI/\u266dII-leaning ones are metal's now and rock keeps the\n\t// \u266dVII-driven ones.\n\tpublic static readonly int[][] RockProgressions =\n\t{\n\t\tnew[] { 0, 5, 6, 0 }, // i\u2013\u266dVI\u2013\u266dVII\u2013i\n\t\tnew[] { 0, 3, 6, 0 }, // i\u2013iv\u2013\u266dVII\u2013i\n\t\tnew[] { 0, 0, 6, 6 }, // i / \u266dVII riff vamp\n\t\tnew[] { 0, 6, 0, 3 }, // i\u2013\u266dVII\u2013i\u2013iv\n\t\tnew[] { 0, 6, 3, 4 }, // i\u2013\u266dVII\u2013iv\u2013v\n\t\tnew[] { 0, 2, 3, 6 }, // i\u2013\u266dIII\u2013iv\u2013\u266dVII\n\t};\n\n\t// Rock: the engine room. Root-driven and locked to the kick, but phrased over two bars so it\n\t// pushes and releases rather than chugging identically forever.\n\tpublic static readonly Pattern[] RockBass =\n\t{\n\t\tP( 0, Rest, 0, 0, Rest, 0, 0, Rest,\n\t\t   0, Rest, 0, 0, Rest, 0, 12, Approach ),                        // syncopated driver (2 bars)\n\t\tP( 0, Rest, Rest, 0, Rest, Rest, 0, Rest,\n\t\t   0, Rest, Rest, 0, Rest, 12, 7, Approach ),                     // dotted push (2 bars)\n\t\tP( 0, 0, 12, 0, 0, 0, 12, Rest,\n\t\t   0, 0, 12, 0, 7, 5, 3, Approach ),                              // octave pushes \u2192 walkdown (2 bars)\n\t\tP( 0, Rest, 0, Rest, 0, Rest, 0, Approach ),                      // quarter pulse (1 bar)\n\t};\n\n\t// \u2500\u2500 Country harmony (Genre 2) \u2500\u2500\n\t// Country is the plainest major genre and stays that way: one mode, and the variety comes from\n\t// the changes and the boom-chick underneath. Mixolydian is ska's \u2014 country and ska sharing\n\t// both bright modes is exactly the near-duplication the cap exists to stop, and a second mode\n\t// bought country nothing its progressions do not already give it.\n\tpublic static readonly int[][] CountryScales =\n\t{\n\t\tnew[] { 0, 2, 4, 5, 7, 9, 11 }, // major\n\t};\n\tpublic static readonly int[] CountryScaleWeights = { 1 };\n\n\t// Country's colour chords are the 6th and the sus4 \u2014 the open, ringing shapes a Telecaster\n\t// plays. No 7ths: that is ska's sound, and a dominant 7th everywhere reads as blues.\n\tpublic static readonly int[][] CountryVoicings = { Triad, Sixth, Sus4 };\n\tpublic static readonly int[] CountryVoicingWeights = { 3, 2, 1 };\n\n\t// Country is the plainest of the major genres \u2014 I, IV and V and not much else \u2014 so it keeps\n\t// the backbone and the two-chord vamps, and the anthem loops go to punk/pop.\n\tpublic static readonly int[][] CountryProgressions =\n\t{\n\t\tnew[] { 0, 3, 4, 0 }, // I\u2013IV\u2013V\u2013I (the country backbone)\n\t\tnew[] { 0, 0, 4, 4 }, // I\u2013V vamp\n\t\tnew[] { 0, 4, 3, 0 }, // I\u2013V\u2013IV\u2013I\n\t\tnew[] { 0, 3, 0, 3 }, // I\u2013IV two-chord\n\t\tnew[] { 0, 4, 0, 4 }, // I\u2013V two-chord\n\t};\n\n\t// Country: \"boom-chick\" \u2014 the bass alternates root and fifth on the beats while the guitar and\n\t// snare take the off \"chick\", and it walks in step to the next chord rather than pushing.\n\t// Two-bar phrases so the walkdown has somewhere to happen.\n\tpublic static readonly Pattern[] CountryBass =\n\t{\n\t\tP( 0, Rest, 7, Rest, 0, Rest, 7, Rest,\n\t\t   0, Rest, 7, Rest, 0, Rest, 5, Approach ),                      // alternating root\u2013fifth (2 bars)\n\t\tP( 0, Rest, 7, Rest, 12, Rest, 7, Rest,\n\t\t   0, Rest, 7, Rest, 9, 7, 5, Approach ),                         // with the octave, walks out (2 bars)\n\t\tP( 0, Rest, 7, Rest, 0, Rest, 7, Rest,\n\t\t   0, Rest, 7, Rest, 4, 5, 7, Approach ),                         // scalar walkup (2 bars)\n\t\tP( 0, Rest, 7, Rest, 0, Rest, 7, Approach ),                      // the plain boom-chick (1 bar)\n\t};\n\n\t// \u2500\u2500 Metal harmony (Genre 3) \u2500\u2500\n\t// Phrygian is the metal mode and carries the weight here; harmonic minor is the neoclassical\n\t// colour. Aeolian stays as the common ground with rock \u2014 one shared mode is honest, three was\n\t// two genres playing the same thing in a different tempo band.\n\tpublic static readonly int[][] MetalScales =\n\t{\n\t\tnew[] { 0, 1, 3, 5, 7, 8, 10 }, // phrygian (the metal mode)\n\t\tnew[] { 0, 2, 3, 5, 7, 8, 10 }, // natural minor (aeolian)\n\t\tnew[] { 0, 2, 3, 5, 7, 8, 11 }, // harmonic minor\n\t};\n\tpublic static readonly int[] MetalScaleWeights = { 4, 2, 1 };\n\n\t// Metal is correctly 3rd-less: the power chord, and the \u266d7 on top of it for the wider riff\n\t// voicing. A major or minor triad through that much gain is mud, and the 3rd is what makes it\n\t// sound like rock rather than metal.\n\tpublic static readonly int[][] MetalVoicings = { Power, PowerFlat7 };\n\tpublic static readonly int[] MetalVoicingWeights = { 4, 1 };\n\n\t// Degrees read against the (minor) scale: 5 = \u266dVI, 6 = \u266dVII, 1 = \u266dII, 3 = iv. Metal takes the\n\t// \u266dVI and phrygian \u266dII moves \u2014 the darkest of the minor turnarounds, and the ones rock does\n\t// not reach for.\n\tpublic static readonly int[][] MetalProgressions =\n\t{\n\t\tnew[] { 0, 6, 5, 6 }, // i\u2013\u266dVII\u2013\u266dVI\u2013\u266dVII (driving)\n\t\tnew[] { 0, 1, 0, 6 }, // i\u2013\u266dII\u2013i\u2013\u266dVII (phrygian menace)\n\t\tnew[] { 0, 0, 5, 6 }, // i pedal \u2192 \u266dVI\u2013\u266dVII\n\t\tnew[] { 0, 5, 1, 0 }, // i\u2013\u266dVI\u2013\u266dII\u2013i\n\t\tnew[] { 0, 0, 1, 1 }, // i / \u266dII pedal riff\n\t\tnew[] { 0, 3, 5, 6 }, // i\u2013iv\u2013\u266dVI\u2013\u266dVII\n\t};\n\n\t// Metal: a low pedal point under the riff, or the riff's own rhythm doubled. These are the\n\t// fallback tables \u2014 when the song draws the \"follows the riff\" mode the bass reads the\n\t// guitar's onsets instead of any of these (see Bass.cs), because both real metal bass modes\n\t// are RELATIONAL and a table can only ever approximate them.\n\tpublic static readonly Pattern[] MetalBass =\n\t{\n\t\tP( 0, 0, 0, 0, 0, 0, 0, 0,\n\t\t   0, 0, 0, 0, 0, 0, 0, Approach ),                               // pedal chug (2 bars)\n\t\tP( 0, Rest, Rest, Rest, Rest, Rest, Rest, Rest,\n\t\t   0, Rest, Rest, Rest, Rest, Rest, Rest, Approach ),             // whole-bar pedal point (2 bars)\n\t\tP( 0, 0, 12, 0, 0, 0, 12, 0,\n\t\t   0, 0, 12, 0, 0, 12, 0, Approach ),                             // octave gallop (2 bars)\n\t};\n\n\t// \u2500\u2500 Punk harmony (Genre 4) \u2500\u2500\n\t// \"Lean punk\" / power-pop: overwhelmingly major, with the minor-key hardcore option as the\n\t// rare draw. Mixolydian went to ska \u2014 punk's grit comes from the tempo and the downstrokes,\n\t// not from a \u266d7.\n\tpublic static readonly int[][] PunkScales =\n\t{\n\t\tnew[] { 0, 2, 4, 5, 7, 9, 11 }, // major (the pop-punk default)\n\t\tnew[] { 0, 2, 3, 5, 7, 8, 10 }, // natural minor (the darker hardcore draw)\n\t};\n\tpublic static readonly int[] PunkScaleWeights = { 5, 1 };\n\n\t// Punk is a power chord and, when it wants the anthem to open up, a plain triad. Nothing\n\t// added, nothing suspended \u2014 the voicing is the least interesting thing about a punk song.\n\tpublic static readonly int[][] PunkVoicings = { Power, Triad };\n\tpublic static readonly int[] PunkVoicingWeights = { 3, 2 };\n\n\t// Major degrees: 3 = IV, 4 = V, 5 = vi \u2014 the anthem turnarounds. Punk keeps the ones that\n\t// start on the tonic and drive; pop keeps the ones that start away from it and loop.\n\tpublic static readonly int[][] PunkProgressions =\n\t{\n\t\tnew[] { 0, 4, 5, 3 }, // I\u2013V\u2013vi\u2013IV (the pop-punk anthem)\n\t\tnew[] { 0, 3, 4, 4 }, // I\u2013IV\u2013V\u2013V\n\t\tnew[] { 0, 4, 3, 4 }, // I\u2013V\u2013IV\u2013V (three-chord drive)\n\t\tnew[] { 0, 5, 4, 3 }, // I\u2013vi\u2013V\u2013IV\n\t\tnew[] { 3, 4, 0, 0 }, // IV\u2013V\u2013I\u2013I (the run-up)\n\t};\n\n\t// Punk: relentless straight eighths \u2014 the one genre where the undifferentiated chug IS the\n\t// idiom. The variation is in the last bar of the phrase, not in the bar-to-bar.\n\tpublic static readonly Pattern[] PunkBass =\n\t{\n\t\tP( 0, 0, 0, 0, 0, 0, 0, 0,\n\t\t   0, 0, 0, 0, 0, 0, 0, 0,\n\t\t   0, 0, 0, 0, 0, 0, 0, 0,\n\t\t   0, 0, 0, 0, 0, 0, 0, Approach ),                               // eighth chug, 4-bar phrase\n\t\tP( 0, 0, 0, 0, 0, 0, 0, 0,\n\t\t   0, 0, 0, 0, 12, 12, 7, Approach ),                             // chug that pops the octave (2 bars)\n\t\tP( 0, 0, 7, 0, 0, 0, 7, Approach ),                               // root\u2013fifth gallop (1 bar)\n\t};\n\n\t// \u2500\u2500 Pop harmony (Genre 5) \u2500\u2500\n\t// Modern synth/dance-pop: bright major, with lydian's \u266f4 as the shimmer. Lydian is pop's\n\t// alone \u2014 it is the one mode that reads as \"produced\" rather than played.\n\tpublic static readonly int[][] PopScales =\n\t{\n\t\tnew[] { 0, 2, 4, 5, 7, 9, 11 }, // major\n\t\tnew[] { 0, 2, 4, 6, 7, 9, 11 }, // lydian (the sparkly \u266f4 \u2014 synth-pop shimmer)\n\t};\n\tpublic static readonly int[] PopScaleWeights = { 3, 1 };\n\n\t// Pop's chords are open and unresolved: add9 and sus2 leave the 3rd ambiguous, which is what\n\t// makes a four-chord loop sound like it never lands. The plain triad is the fallback.\n\tpublic static readonly int[][] PopVoicings = { Add9, Sus2, Triad };\n\tpublic static readonly int[] PopVoicingWeights = { 3, 2, 3 };\n\n\t// Pop owns the loops that do not begin on the tonic \u2014 the \"Axis\" rotations, which is exactly\n\t// what makes a four-chord pop loop sound endless rather than resolved.\n\tpublic static readonly int[][] PopProgressions =\n\t{\n\t\tnew[] { 5, 3, 0, 4 }, // vi\u2013IV\u2013I\u2013V (the \"Axis\" loop)\n\t\tnew[] { 0, 5, 3, 4 }, // I\u2013vi\u2013IV\u2013V\n\t\tnew[] { 3, 0, 4, 5 }, // IV\u2013I\u2013V\u2013vi\n\t\tnew[] { 0, 3, 5, 4 }, // I\u2013IV\u2013vi\u2013V\n\t\tnew[] { 5, 3, 4, 0 }, // vi\u2013IV\u2013V\u2013I\n\t};\n\n\t// Pop: a synth bass locked to the four-on-the-floor kick, with the octave pops that give a\n\t// dance track its bounce. One chord per bar here (ChordBars = 1), so these stay short and the\n\t// approach fires into every change.\n\tpublic static readonly Pattern[] PopBass =\n\t{\n\t\tP( 0, Rest, 0, Rest, 0, Rest, 0, Approach ),                      // root on every beat (1 bar)\n\t\tP( 0, Rest, 12, 0, Rest, 12, 0, Rest,\n\t\t   0, Rest, 12, 0, Rest, 12, 7, Approach ),                       // octave pops (2 bars)\n\t\tP( 0, Rest, Rest, 0, Rest, 0, Rest, Approach ),                   // sidechained syncopation (1 bar)\n\t};\n\n\t/// <summary>Degree \u2192 MIDI pitch against <paramref name=\"scale\"/>, wrapping octaves in both\n\t/// directions so any degree resolves.</summary>\n\t/// <summary>\n\t/// How far a bend travels, in semitones, from a note sitting <paramref name=\"pc\"/> semitones\n\t/// above the tonic: to the nearest tone OF THE SCALE at or beyond <paramref name=\"depth\"/>\n\t/// semitones up, and never back to the note it started from. 0 means nothing in reach.\n\t///\n\t/// A PLAYER BENDS TO A NOTE, NOT BY AN INTERVAL. The string arrives at the next tone of the\n\t/// scale, which is a whole step in some places and a semitone in others \u2014 the same fact about\n\t/// seven-note scales that <see cref=\"VoicedTone\"/> exists for, reached through the melody\n\t/// instead of through a chord. Bent by a fixed interval the note lands off the key on every\n\t/// degree whose step is the other size: a whole step off the third or the seventh of a major\n\t/// scale, a semitone off almost anywhere. It is worst on a bend that is HELD, because the note\n\t/// then spends its whole tail outside the key rather than passing through it \u2014 which is what\n\t/// \"out of tune\" sounds like when nothing has actually been mistuned.\n\t///\n\t/// Depth stays the instrument's PREFERENCE \u2014 how far the hand reaches, which is the thing a\n\t/// genre has an opinion about \u2014 rather than the distance the pitch travels.\n\t/// </summary>\n\tpublic static int BendSemis( int[] scale, int pc, float depth )\n\t{\n\t\tint want = Math.Max( 1, (int)MathF.Round( depth ) );\n\t\tint best = 0, bestCost = int.MaxValue;\n\t\tfor ( int s = 1; s <= BendReach; s++ )\n\t\t{\n\t\t\tbool inKey = false;\n\t\t\tforeach ( int t in scale )\n\t\t\t\tif ( (((t % 12) + 12) % 12) == (pc + s) % 12 ) { inKey = true; break; }\n\t\t\tif ( !inKey ) continue;\n\t\t\tint cost = Math.Abs( s - want );\n\t\t\tif ( cost >= bestCost ) continue;\n\t\t\tbestCost = cost; best = s;\n\t\t}\n\t\treturn best;\n\t}\n\n\t/// <summary>How far up a bend will look for a tone of the scale. A seven-note scale has one\n\t/// within two semitones of anywhere, so this is slack for the pentatonic and blues tables\n\t/// rather than a range a bend actually reaches \u2014 past it there is no note to arrive at and\n\t/// the bend does not happen.</summary>\n\tpublic const int BendReach = 4;\n\n\tpublic static int ScaleMidi( int baseMidi, int[] scale, int degree )\n\t{\n\t\tint len = scale.Length;\n\t\tint oct = (int)Math.Floor( degree / (double)len );\n\t\treturn baseMidi + scale[degree - oct * len] + 12 * oct;\n\t}\n\n\t/// <summary>Root pitch of a progression degree.</summary>\n\tpublic static int ChordRoot( int rootMidi, int[] scale, int degree )\n\t\t=> ScaleMidi( rootMidi, scale, degree );\n\n\t/// <summary>\n\t/// One voice of a chord, as a MIDI pitch \u2014 the ONLY correct way to turn a voicing into notes.\n\t///\n\t/// A voicing is a list of degree offsets, so <c>ScaleMidi(base, root + offset)</c> spells it\n\t/// DIATONICALLY: every interval comes out whatever the scale makes it at that degree. For the\n\t/// third, the sixth, the seventh and the ninth that is exactly right \u2014 major-or-minor by\n\t/// position is what diatonic harmony IS. For the FOURTH and the FIFTH it is wrong, because\n\t/// every seven-note scale has one degree whose diatonic fifth is DIMINISHED and one whose\n\t/// fourth is AUGMENTED. Spelled diatonically, a power chord on that degree is a bare tritone\n\t/// and a sus4 is a root with a flat five \u2014 with no third present to explain either, which is\n\t/// what \"way off key\" sounds like, and it is at its worst through distortion.\n\t///\n\t/// A guitarist frets the same power-chord shape on every degree; the shape does not go\n\t/// diminished because of the key. So the fourth and the fifth are forced perfect and\n\t/// everything else keeps its diatonic spelling. Both offsets sit inside one octave of the\n\t/// root, so the perfect interval is simply the root plus 5 or 7.\n\t/// </summary>\n\tpublic static int VoicedTone( int baseMidi, int[] scale, int rootDegree, int offset )\n\t{\n\t\tint root = ScaleMidi( baseMidi, scale, rootDegree );\n\t\tif ( offset == Fourth ) return root + 5;\n\t\tif ( offset == Fifth ) return root + 7;\n\t\treturn ScaleMidi( baseMidi, scale, rootDegree + offset );\n\t}\n\n\t/// <summary>How far one voice may be octave-shifted to stay near the chord before it. An\n\t/// octave is what an INVERSION is; more than that moves the part into another register\n\t/// rather than re-voicing the chord.</summary>\n\tpublic const int MaxVoiceLead = 12;\n\n\t/// <summary>\n\t/// The song's chord plan: for every chord of the progression, WHICH note of the voicing each\n\t/// voice takes and how far it is octave-shifted.\n\t///\n\t/// The two are one decision. Octave-shifting alone kills the octave-sized parallel leap but\n\t/// leaves voice <c>i</c> permanently on the <c>i</c>th offset of the voicing, so a root move of\n\t/// a fourth still moves every voice by that fourth \u2014 a smaller parallel slide rather than none,\n\t/// and no common tone anywhere in it. Letting the chord ROTATE is what produces a common tone:\n\t/// voice <c>i</c> plays offset <c>(i + Rot[c]) mod n</c>, so the voice that was on the fifth can\n\t/// take the new chord's root and simply stay where it is.\n\t/// </summary>\n\tpublic readonly struct VoicePlan\n\t{\n\t\t/// <summary>Per chord, per voice: the octave offset in semitones.</summary>\n\t\tpublic readonly int[][] Shift;\n\n\t\t/// <summary>Per chord: voice <c>i</c> plays voicing offset <c>(i + Rot[c]) mod n</c>.</summary>\n\t\tpublic readonly int[] Rot;\n\n\t\tpublic VoicePlan( int[][] shift, int[] rot ) { Shift = shift; Rot = rot; }\n\t}\n\n\t/// <summary>\n\t/// VOICE LEADING: per chord of <paramref name=\"prog\"/>, the octave offset each voice of\n\t/// <paramref name=\"voicing\"/> takes so the chord sits near the one before it.\n\t///\n\t/// Built upward from its root degree, a chord's register is wherever that degree happens to\n\t/// fall and the same shape simply slides \u2014 so a progression that steps a third moves every\n\t/// voice a tenth, with no common tone, every time the change comes round. That is what reads\n\t/// as \"it jumped\" (and it is loudest when a chord change lands on a section boundary). A\n\t/// player inverts instead: keep the register, keep the common tones, move the voices that\n\t/// have to move.\n\t///\n\t/// Each voice is octave-shifted to whichever octave sits nearest its OWN previous pitch (ties\n\t/// going to the octave nearer root position, so the comp cannot walk itself out of register\n\t/// over a few laps of the progression), so\n\t/// the chord's identity is untouched \u2014 same degrees, same spelling, different inversion. The\n\t/// result is a table because it is a property of the SONG (progression \u00d7 voicing \u00d7 scale),\n\t/// not of a voice: every chordal voice reads the same shifts and therefore agrees on the\n\t/// inversion, whatever register it plays in. The bass is deliberately NOT in here \u2014 it plays\n\t/// roots, and a root that inverts is a different chord.\n\t///\n\t/// A PROGRESSION IS A CYCLE, and the choice is made as one: the last chord going back round to\n\t/// the first is a change like any other, so a greedy walk anchored at the first chord parks\n\t/// every leap the other three avoided on that one seam. Relaxing the walk round and round does\n\t/// not fix it either \u2014 the cycle is what makes it oscillate rather than settle, and an\n\t/// unsettled seam is a leap of a seventh sitting in the middle of a fixed table. So each voice\n\t/// is solved EXACTLY, by a walk over the three octaves it may take at each chord that closes\n\t/// the loop (voices are independent, three options each, four chords: it is a handful of\n\t/// additions, done once per song). Ties go to the octave nearer root position, so the comp\n\t/// cannot walk itself out of register.\n\t///\n\t/// Pitches are measured over a base of 0 \u2014 the shift is base-independent \u2014 and a voice may dip\n\t/// a little under that base but no further (<see cref=\"VoiceLeadFloor\"/>).\n\t/// </summary>\n\tpublic static VoicePlan PlanVoiceLeading( int[] scale, int[] prog, int[] voicing )\n\t{\n\t\tint n = voicing.Length, np = prog.Length;\n\t\tvar shift = new int[np][];\n\t\tfor ( int c = 0; c < np; c++ ) shift[c] = new int[n];\n\n\t\t// Root-position pitch of every voicing slot at every chord \u2014 the raw material both passes\n\t\t// below read.\n\t\tvar tone = new int[np, n];\n\t\tfor ( int c = 0; c < np; c++ )\n\t\t\tfor ( int s = 0; s < n; s++ )\n\t\t\t\ttone[c, s] = VoicedTone( 0, scale, prog[c], voicing[s] );\n\n\t\tvar rot = PlanRotation( tone, np, n );\n\n\t\tint k = 2 * (MaxVoiceLead / 12) + 1;              // the octaves on offer: \u22121, 0, +1\n\t\tvar raw = new int[np];\n\t\tvar cost = new int[np, k];\n\t\tvar from = new int[np, k];\n\t\tvar chain = new int[np];\n\n\t\tfor ( int v = 0; v < n; v++ )\n\t\t{\n\t\t\t// Given the rotation, the voices are independent again: voice v simply plays whichever\n\t\t\t// slot the rotation hands it at each chord, and chooses its own octaves over that line.\n\t\t\tfor ( int c = 0; c < np; c++ ) raw[c] = tone[c, (v + rot[c]) % n];\n\n\t\t\tint bestTotal = int.MaxValue;\n\t\t\tfor ( int start = 0; start < k; start++ )     // the loop has to close on what it opened\n\t\t\t{\n\t\t\t\tif ( Pitch( raw[0], start ) < VoiceLeadFloor ) continue;\n\t\t\t\tfor ( int c = 0; c < np; c++ )\n\t\t\t\t\tfor ( int o = 0; o < k; o++ ) { cost[c, o] = Unreachable; from[c, o] = 0; }\n\t\t\t\tcost[0, start] = Home( start );\n\n\t\t\t\tfor ( int c = 1; c < np; c++ )\n\t\t\t\t\tfor ( int o = 0; o < k; o++ )\n\t\t\t\t\t{\n\t\t\t\t\t\tif ( Pitch( raw[c], o ) < VoiceLeadFloor ) continue;\n\t\t\t\t\t\tfor ( int p = 0; p < k; p++ )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tif ( cost[c - 1, p] >= Unreachable ) continue;\n\t\t\t\t\t\t\tint t = cost[c - 1, p] + Move( raw[c - 1], p, raw[c], o ) + Home( o );\n\t\t\t\t\t\t\tif ( t >= cost[c, o] ) continue;\n\t\t\t\t\t\t\tcost[c, o] = t;\n\t\t\t\t\t\t\tfrom[c, o] = p;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\tfor ( int last = 0; last < k; last++ )\n\t\t\t\t{\n\t\t\t\t\tif ( cost[np - 1, last] >= Unreachable ) continue;\n\t\t\t\t\tint total = cost[np - 1, last]\n\t\t\t\t\t\t+ (np > 1 ? Move( raw[np - 1], last, raw[0], start ) : 0);\n\t\t\t\t\tif ( total >= bestTotal ) continue;\n\t\t\t\t\tbestTotal = total;\n\t\t\t\t\tfor ( int c = np - 1, o = last; c >= 0; c-- ) { chain[c] = o; o = from[c, o]; }\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor ( int c = 0; c < np; c++ ) shift[c][v] = 12 * (chain[c] - MaxVoiceLead / 12);\n\t\t}\n\t\treturn new VoicePlan( shift, rot );\n\n\t\tint Pitch( int r, int o ) => r + 12 * (o - MaxVoiceLead / 12);\n\t\t// Motion is weighted so it always outranks the pull toward root position: an octave of\n\t\t// register is worth having, but never at the price of a semitone of extra movement.\n\t\tint Move( int ra, int a, int rb, int b ) => 2 * Math.Abs( Pitch( rb, b ) - Pitch( ra, a ) );\n\t\tint Home( int o ) => Math.Abs( o - MaxVoiceLead / 12 );\n\t}\n\n\t/// <summary>\n\t/// Which note of the voicing each voice takes, per chord \u2014 the rotation half of the plan.\n\t///\n\t/// Solved BEFORE the octaves and separately from them, which is what keeps the whole thing\n\t/// cheap. Solving both at once would make the state a rotation plus an octave for every voice\n\t/// (3^n \u00d7 n rotations per chord), because a voice's octave at one chord is paid for on the edges\n\t/// either side of it. Split, each pass is small: this one walks n rotations over np chords, and\n\t/// the octave pass is then per-voice independent again.\n\t///\n\t/// The split is honest because of what this pass measures. It does not know the octaves yet, so\n\t/// it costs a voice's move as the distance it would travel IF it may invert freely \u2014 the\n\t/// interval folded into a tritone either way. That is exactly the question a rotation answers\n\t/// (\"can this voice hold a common tone, or must it move?\") and exactly the question the octave\n\t/// pass then answers concretely. A rotation that leaves a voice on the same pitch class costs\n\t/// zero here, which is a common tone, which is the point of the row.\n\t///\n\t/// A PROGRESSION IS A CYCLE, on the same argument as the octave pass: solved with the wrap from\n\t/// the last chord back to the first included, or every rotation the other changes avoided parks\n\t/// itself on that seam. Ties pull toward rotation 0, so a plan that gains nothing from rotating\n\t/// simply doesn't, and the voicing's own order \u2014 root at the bottom, as its table is written \u2014\n\t/// survives wherever it is free to.\n\t/// </summary>\n\tstatic int[] PlanRotation( int[,] tone, int np, int n )\n\t{\n\t\tvar rot = new int[np];\n\t\tif ( n < 2 || np < 2 ) return rot;\n\n\t\tvar cost = new int[np, n];\n\t\tvar from = new int[np, n];\n\t\tint bestTotal = int.MaxValue;\n\n\t\tfor ( int start = 0; start < n; start++ )\n\t\t{\n\t\t\tfor ( int c = 0; c < np; c++ )\n\t\t\t\tfor ( int r = 0; r < n; r++ ) { cost[c, r] = Unreachable; from[c, r] = 0; }\n\t\t\tcost[0, start] = Home( start );\n\n\t\t\tfor ( int c = 1; c < np; c++ )\n\t\t\t\tfor ( int r = 0; r < n; r++ )\n\t\t\t\t\tfor ( int p = 0; p < n; p++ )\n\t\t\t\t\t{\n\t\t\t\t\t\tif ( cost[c - 1, p] >= Unreachable ) continue;\n\t\t\t\t\t\tint t = cost[c - 1, p] + Move( c - 1, p, c, r ) + Home( r );\n\t\t\t\t\t\tif ( t >= cost[c, r] ) continue;\n\t\t\t\t\t\tcost[c, r] = t;\n\t\t\t\t\t\tfrom[c, r] = p;\n\t\t\t\t\t}\n\n\t\t\tfor ( int last = 0; last < n; last++ )\n\t\t\t{\n\t\t\t\tif ( cost[np - 1, last] >= Unreachable ) continue;\n\t\t\t\tint total = cost[np - 1, last] + Move( np - 1, last, 0, start );\n\t\t\t\tif ( total >= bestTotal ) continue;\n\t\t\t\tbestTotal = total;\n\t\t\t\tfor ( int c = np - 1, r = last; c >= 0; c-- ) { rot[c] = r; r = from[c, r]; }\n\t\t\t}\n\t\t}\n\t\treturn rot;\n\n\t\t// Motion outranks the pull toward the unrotated order, the same way it does for octaves.\n\t\tint Move( int a, int ra, int b, int rb )\n\t\t{\n\t\t\tint sum = 0;\n\t\t\tfor ( int v = 0; v < n; v++ )\n\t\t\t\tsum += Fold( tone[b, (v + rb) % n] - tone[a, (v + ra) % n] );\n\t\t\treturn 2 * sum;\n\t\t}\n\t\tint Home( int r ) => r == 0 ? 0 : 1;\n\t\t// The interval a freely-inverting voice would actually travel: a minor seventh up is a whole\n\t\t// tone down, and the octave pass is what will choose which.\n\t\tstatic int Fold( int d )\n\t\t{\n\t\t\td = ((d % 12) + 12) % 12;\n\t\t\treturn Math.Min( d, 12 - d );\n\t\t}\n\t}\n\n\t/// <summary>\n\t/// Octave offsets that put a chord built on <paramref name=\"rootDegree\"/> as near as possible to\n\t/// the pitches it follows \u2014 the one-off version of the plan above, for a chord that has no slot\n\t/// in the progression's cycle.\n\t///\n\t/// The ENDING is what needs it. A song's last chord used to be built in root position on the\n\t/// argument that a song should land where its genre voices the chord rather than where the last\n\t/// change happened to leave the register \u2014 which is a reasonable thing to want and was still\n\t/// wrong, because it put the only unled change in the song on its most exposed moment, and a\n\t/// seventh-sized leap into the final chord is heard by everyone. The register a song has been\n\t/// in for three minutes IS where it should land; a cadence is a change like any other, and the\n\t/// ritard is not a licence to jump.\n\t/// </summary>\n\tpublic static int[] LeadToward( int[] scale, int[] prev, int baseMidi, int rootDegree, int[] voicing )\n\t{\n\t\tvar shift = new int[voicing.Length];\n\t\tif ( prev == null || prev.Length == 0 ) return shift;\n\t\tfor ( int i = 0; i < voicing.Length; i++ )\n\t\t{\n\t\t\tint raw = VoicedTone( baseMidi, scale, rootDegree, voicing[i] );\n\t\t\t// Nearest to the voice that was on the same line, where there was one; the chord may be\n\t\t\t// spelled with more notes than the one before it (a suspension resolving, a driven\n\t\t\t// guitar), so anything past the end leans on the top voice.\n\t\t\tint target = prev[Math.Min( i, prev.Length - 1 )];\n\t\t\tint best = 0, bestCost = int.MaxValue;\n\t\t\tfor ( int o = -MaxVoiceLead; o <= MaxVoiceLead; o += 12 )\n\t\t\t{\n\t\t\t\tif ( raw + o < baseMidi + VoiceLeadFloor ) continue;\n\t\t\t\tint cost = 2 * Math.Abs( raw + o - target ) + Math.Abs( o ) / 12;\n\t\t\t\tif ( cost >= bestCost ) continue;\n\t\t\t\tbestCost = cost; best = o;\n\t\t\t}\n\t\t\tshift[i] = best;\n\t\t}\n\t\treturn shift;\n\t}\n\n\t/// <summary>How far under its own base a voice may be led, relative to the base the chord is\n\t/// voiced up from. A chord whose root is the scale's seventh degree sits eleven semitones up in\n\t/// root position and one semitone DOWN inverted, and the inverted one is the whole point \u2014 a\n\t/// floor at the base exactly forbids the move that helps most. Half an octave under is where\n\t/// \"inverted\" turns into \"an octave lower\", and metal's comp is based at the bass's own\n\t/// register, so there is no room below that.</summary>\n\tpublic const int VoiceLeadFloor = -6;\n\n\t/// <summary>Cost of a path that does not exist \u2014 larger than any real one, and small enough to\n\t/// add to another without overflowing.</summary>\n\tconst int Unreachable = 1 << 20;\n}\n\npublic sealed partial class MusicGen\n{\n\t// The song's own scale/progression, supplied to the stateless Harmony maths. Every voice\n\t// calls these rather than reaching for _scale directly. The section's KeyShift rides on the\n\t// root here, so a modulation moves the whole band at once (see Part.KeyShift).\n\tint ScaleMidi( int baseMidi, int degree ) => Harmony.ScaleMidi( baseMidi, _scale, degree );\n\n\t/// <summary>A voice's register: the pitch it spells its scale over, <paramref name=\"octaves\"/>\n\t/// octaves above the song's root.\n\t///\n\t/// A REGISTER IS A NUMBER OF OCTAVES, AND ONLY EVER THAT. <see cref=\"Harmony.ScaleMidi\"/> and\n\t/// <see cref=\"Harmony.VoicedTone\"/> treat their base as THE TONIC and add the scale offset on\n\t/// top, so a base of root + 31 does not raise a part by a fifth \u2014 it spells that part in the\n\t/// key a fifth up. The part then disagrees with the band about one note of the scale (about two\n\t/// of them, a whole tone up), and a melody in a different key from its backing is exactly what\n\t/// it sounds like. Every voice takes its register through here so a base that is not a whole\n\t/// octave cannot be written in the first place, which is worth more than a test for it: the\n\t/// wrong version stays in tune roughly six notes in seven, so it does not announce itself.\n\t///\n\t/// The cost is that register is QUANTISED \u2014 a part sits an octave up or it doesn't, and there\n\t/// is no landing between. If a part ends up too high, narrow what it plays (a melody's degree\n\t/// range) rather than reaching for a base between two octaves.\n\t///\n\t/// Transposing an actual PITCH by an octave (<c>ChordRoot(c) + 12</c>) is a different thing and\n\t/// is fine \u2014 the scale has already been spelled by then.</summary>\n\tint Register( int octaves ) => _rootMidi + _keyShift + 12 * octaves;\n\tint ChordRoot( int c ) => Harmony.ChordRoot( _rootMidi + _keyShift, _scale, _prog[c] );\n\n\t/// <summary>Degree of the <paramref name=\"i\"/>th voice of the chord, in the song's own\n\t/// voicing. Indices past the top wrap up an octave, so an arpeggio can just keep counting.\n\t/// </summary>\n\tint ChordDegree( int chord, int i )\n\t{\n\t\tint n = _voicing.Length;\n\t\tint oct = (int)Math.Floor( i / (double)n );\n\t\treturn _prog[chord] + _voicing[i - oct * n] + oct * _scale.Length;\n\t}\n\n\t/// <summary>Every note of the chord, as scale degrees. This is the MELODIC view \u2014 what tones\n\t/// a line may land on. A voice that SOUNDS the chord wants <see cref=\"ChordMidis\"/> instead,\n\t/// which keeps the perfect intervals perfect.</summary>\n\tint[] ChordDegrees( int chord )\n\t{\n\t\tvar d = new int[_voicing.Length];\n\t\tfor ( int i = 0; i < d.Length; i++ ) d[i] = _prog[chord] + _voicing[i];\n\t\treturn d;\n\t}\n\n\t/// <summary>Every note of the chord as MIDI pitches over <paramref name=\"baseMidi\"/> \u2014 what a\n\t/// chordal voice actually plays. Use this rather than <c>ScaleMidi</c> over\n\t/// <see cref=\"ChordDegrees\"/>: see <see cref=\"Harmony.VoicedTone\"/> for why the difference\n\t/// matters on one degree of every scale.\n\t///\n\t/// This is also where the song's VOICE LEADING lands (<see cref=\"Harmony.PlanVoiceLeading\"/>),\n\t/// so the chord arrives in the inversion nearest the one before it instead of sliding a tenth.\n\t/// The array index is a VOICE, not a voicing slot \u2014 the plan rotates, so which offset voice i\n\t/// plays is <c>(i + Rot[chord]) mod n</c> and changes chord to chord. Anything that needs to\n\t/// know what a pitch IS asks <see cref=\"ChordOffsets\"/> for the matching offsets rather than\n\t/// indexing <c>_voicing</c> alongside it; the driven guitar is the one that does.</summary>\n\tint[] ChordMidis( int baseMidi, int chord, int tick )\n\t{\n\t\tvar voicing = VoicingAt( tick );\n\t\tint n = voicing.Length, r = _vlRot[chord];\n\t\tvar s = _vlShift[chord];\n\t\tvar m = new int[n];\n\t\tfor ( int i = 0; i < n; i++ )\n\t\t\tm[i] = Harmony.VoicedTone( baseMidi, _scale, _prog[chord], voicing[(i + r) % n] ) + s[i];\n\t\treturn m;\n\t}\n\n\t/// <summary>The voicing offsets <see cref=\"ChordMidis\"/> just spelled, in the same order \u2014 what\n\t/// each of its pitches IS. A rotated chord has to carry these alongside its pitches, because\n\t/// the array index no longer names the voicing slot: without them the driven guitar drops\n\t/// whatever happens to be third in the array rather than the chord's third.</summary>\n\tint[] ChordOffsets( int chord, int tick )\n\t{\n\t\tvar voicing = VoicingAt( tick );\n\t\tint n = voicing.Length, r = _vlRot[chord];\n\t\tvar o = new int[n];\n\t\tfor ( int i = 0; i < n; i++ ) o[i] = voicing[(i + r) % n];\n\t\treturn o;\n\t}\n\n\t/// <summary>The spelling the chordal voices sound at <paramref name=\"tick\"/>: the song's\n\t/// voicing, or \u2014 over the back half of the current chord's span \u2014 the one its suspension\n\t/// resolves to (<see cref=\"Harmony.SuspendedVoice\"/>). The two arrays are the same object\n\t/// unless the voicing is suspended, so this costs nothing for the other five voicings.\n\t///\n\t/// EVERY chordal voice reads it, at the tick of the note it is about to sound, so the band\n\t/// resolves together the way it agrees on the chord and the inversion. The voice-leading table\n\t/// is deliberately NOT recomputed: a suspension resolving moves one voice a step inside the\n\t/// inversion the song already chose, which is what a player's finger does \u2014 re-inverting the\n\t/// chord underneath it would make the landing a jump.</summary>\n\tint[] VoicingAt( int tick ) => tick >= _susResolveTick ? _voicingRes : _voicing;\n\n\t/// <summary>A chordal note of <paramref name=\"durTicks\"/> from <paramref name=\"tick\"/>, split\n\t/// into the part before its suspension resolves and the part after \u2014 one segment for every\n\t/// note that does not straddle the resolution, which is all of them for a voicing that is not\n\t/// suspended.\n\t///\n\t/// The chord is RE-ARTICULATED where the third lands rather than changing under a ringing\n\t/// note, because a suspension that resolves silently is not heard to resolve: the landing is\n\t/// the gesture, and a player re-picks or hammers the note that moves. It matters most where a\n\t/// voice holds a whole chord at a time \u2014 pop's pad sounds one chord per bar, so without this\n\t/// its suspension has nowhere to land at all.\n\t///\n\t/// The chordal voices whose genres can draw a suspension (the guitar and the keys) read this;\n\t/// ska's skank and horns do not, because every ska voicing states its third.</summary>\n\tIEnumerable<(int Tick, int Ticks)> ChordSegments( int tick, int durTicks )\n\t{\n\t\tif ( _susVoice >= 0 && tick < _susResolveTick && tick + durTicks > _susResolveTick )\n\t\t{\n\t\t\tyield return (tick, _susResolveTick - tick);\n\t\t\tyield return (_susResolveTick, tick + durTicks - _susResolveTick);\n\t\t\tyield break;\n\t\t}\n\t\tyield return (tick, durTicks);\n\t}\n\n\t/// <summary>As <see cref=\"ChordMidis\"/> but in ROOT POSITION, for a chord whose root degree is\n\t/// given directly \u2014 the ending's cadence builds its V that way, and a final chord lands where\n\t/// the genre voices it rather than where the last change left the register.</summary>\n\tint[] VoicedMidis( int baseMidi, int rootDegree, int[] voicing )\n\t{\n\t\tvar m = new int[voicing.Length];\n\t\tfor ( int i = 0; i < m.Length; i++ )\n\t\t\tm[i] = Harmony.VoicedTone( baseMidi, _scale, rootDegree, voicing[i] );\n\t\treturn m;\n\t}\n\n\t/// <summary>Pitch of the <paramref name=\"i\"/>th voice of the chord (wrapping up an octave past\n\t/// the top, so an arpeggio can keep counting) \u2014 the sounding counterpart of\n\t/// <see cref=\"ChordDegree\"/>.</summary>\n\tint ChordToneMidi( int baseMidi, int chord, int i, int tick )\n\t{\n\t\tvar voicing = VoicingAt( tick );\n\t\tint n = voicing.Length;\n\t\tint oct = (int)Math.Floor( i / (double)n );\n\t\tint v = i - oct * n;\n\t\treturn Harmony.VoicedTone( baseMidi, _scale, _prog[chord], voicing[(v + _vlRot[chord]) % n] )\n\t\t\t+ _vlShift[chord][v] + 12 * oct;\n\t}\n}\n"
        },
        {
            "Ident": "gamah.skafinity",
            "Path": "Engine/Rng.cs",
            "FileName": "Rng.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 341414,
            "IsPrivate": false,
            "Code": "using System;\n\nnamespace Skafinity;\n\n/// <summary>\n/// The engine's PRNG \u2014 xmur3 hashes a seed string to 32 bits, mulberry32 streams from it.\n/// Every musical choice comes out of here, which is why one seed gives one song.\n///\n/// Deliberately hand-rolled rather than <c>System.Random</c>: the algorithm is pinned by this\n/// file, so it does not move under us when a runtime changes its implementation, and the same\n/// source gives the same stream in the s&amp;box library and the wasm bundle alike.\n///\n/// Streams are cheap and are meant to be forked liberally \u2014 the composer gives each voice in\n/// each section its own <c>Rng</c> keyed on the section, so adding a draw to one voice cannot\n/// shift what any other voice plays.\n/// </summary>\nsealed class Rng\n{\n\tuint _a;\n\n\tpublic Rng( uint seed ) { _a = seed; }\n\n\t/// <summary>A stream keyed on a string \u2014 the composer's usual entry point\n\t/// (<c>\"{tag}:bass:{section}\"</c> and friends).</summary>\n\tpublic Rng( string seed ) : this( Xmur3( seed ) ) { }\n\n\t/// <summary>xmur3: string \u2192 a well-mixed 32-bit seed.</summary>\n\tpublic static uint Xmur3( string str )\n\t{\n\t\tuint h = 1779033703u ^ (uint)str.Length;\n\t\tfor ( int i = 0; i < str.Length; i++ )\n\t\t{\n\t\t\th = unchecked( (h ^ str[i]) * 3432918353u );\n\t\t\th = (h << 13) | (h >> 19);\n\t\t}\n\t\th = unchecked( (h ^ (h >> 16)) * 2246822507u );\n\t\th = unchecked( (h ^ (h >> 13)) * 3266489909u );\n\t\treturn h ^ (h >> 16);\n\t}\n\n\t/// <summary>mulberry32: the next value in [0, 1).</summary>\n\tpublic float Next()\n\t{\n\t\t_a = unchecked( _a + 0x6D2B79F5u );\n\t\tuint t = _a;\n\t\tt = unchecked( (t ^ (t >> 15)) * (t | 1u) );\n\t\tt ^= unchecked( t + (t ^ (t >> 7)) * (t | 61u) );\n\t\treturn (t ^ (t >> 14)) / 4294967296f;\n\t}\n\n\t/// <summary>A value in [0, n). Clamped, so it is always a safe table index.</summary>\n\tpublic int Int( int n ) => n <= 0 ? 0 : Math.Min( n - 1, (int)(Next() * n) );\n\n\tpublic bool Chance( float p ) => Next() < p;\n\n\tpublic T Pick<T>( T[] arr ) => arr[Int( arr.Length )];\n\n\t/// <summary>A weighted draw \u2014 ONE <see cref=\"Next\"/> whatever the weights are.\n\t///\n\t/// The tables used to bias a draw by listing an entry twice, which quietly tied \"how likely\"\n\t/// to \"how many entries\" (and to the no-two-genres-share-more-than-one cap the engine test\n\t/// enforces). A real weighted draw separates them, and costs the same single value out of\n\t/// the song stream, so a genre's draw count still cannot depend on its tables.</summary>\n\t/// <summary>The INDEX of a weighted draw \u2014 one <see cref=\"Next\"/>, like\n\t/// <see cref=\"PickWeighted\"/>, for callers whose table is a parallel array rather than the\n\t/// thing being picked (the melody's note lengths against a genre's weights over them).</summary>\n\tpublic int WeightedIndex( int[] weights )\n\t{\n\t\tif ( weights == null || weights.Length == 0 ) return 0;\n\t\tint total = 0;\n\t\tforeach ( var w in weights ) total += Math.Max( 0, w );\n\t\tif ( total <= 0 ) return Int( weights.Length );\n\t\tfloat r = Next() * total;\n\t\tfor ( int i = 0; i < weights.Length; i++ )\n\t\t{\n\t\t\tr -= Math.Max( 0, weights[i] );\n\t\t\tif ( r < 0f ) return i;\n\t\t}\n\t\treturn weights.Length - 1;\n\t}\n\n\tpublic T PickWeighted<T>( T[] arr, int[] weights )\n\t{\n\t\tif ( arr == null || arr.Length == 0 ) return default;\n\t\tif ( weights == null || weights.Length != arr.Length ) return Pick( arr );\n\t\tint total = 0;\n\t\tforeach ( var w in weights ) total += Math.Max( 0, w );\n\t\tif ( total <= 0 ) return Pick( arr );\n\t\tfloat r = Next() * total;\n\t\tfor ( int i = 0; i < arr.Length; i++ )\n\t\t{\n\t\t\tr -= Math.Max( 0, weights[i] );\n\t\t\tif ( r < 0f ) return arr[i];\n\t\t}\n\t\treturn arr[arr.Length - 1];\n\t}\n}\n"
        },
        {
            "Ident": "gamah.skafinity",
            "Path": "Engine/SeedCodec.cs",
            "FileName": "SeedCodec.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 341414,
            "IsPrivate": false,
            "Code": "using System;\nusing System.Text;\n\nnamespace Skafinity;\n\n/// <summary>\n/// The seed STRING \u2014 what a whole song is, and the only thing that has to travel between two\n/// people for them to hear the same thing.\n///\n///   <c>tag:n[:genre][:vibe]</c>\n///\n/// * <c>tag</c> \u2014 the station: <c>[A-Za-z0-9_-]</c> only. Anything else (a colon, a space, a\n///   slash) is a parse ERROR, never a coerced string, because a seed that quietly becomes a\n///   different seed is worse than one that is refused.\n/// * <c>n</c> \u2014 the song index in that station's endless line, and it keeps the job it has always\n///   had: Prev/Next are n\u00b11, the look-ahead queue walks an ordered timeline of them, and n is what\n///   lets you go back to a song fifty ago that nothing anywhere remembers. Optional in the string\n///   (a bare <c>tag</c> is song 0 of that station), because typing a station name is how you go\n///   somewhere new.\n/// * <c>genre</c> and <c>vibe</c> \u2014 both optional, both hex, and ORDER-FREE: they are told apart\n///   by length. One char is a genre; <see cref=\"VibeCodec.VibeLength\"/> chars is a vibe. Any other\n///   length is an error rather than a guess.\n///\n/// ABSENT MEANS ROLLED. An omitted genre or vibe is derived deterministically from (tag, n), so it\n/// changes with every song and the station stays a station. Present means PINNED. The two roll\n/// from separate streams, so pinning one does not move the other: pin a vibe and let genres roll,\n/// pin a genre and let vibes roll, pin both and move only n.\n///\n/// That is why the vibe is genre-independent and full width (see <see cref=\"VibeCodec\"/>) \u2014 a\n/// pinned vibe has to mean the same thing under a genre that rolled out from under it.\n/// </summary>\npublic static class SeedCodec\n{\n\t/// <summary>No genre pinned \u2014 roll it from (tag, n).</summary>\n\tpublic const int RolledGenre = -1;\n\n\t/// <summary>A parsed seed. <see cref=\"Genre\"/> is <see cref=\"RolledGenre\"/> and\n\t/// <see cref=\"Vibe\"/> is null where the string pinned nothing.</summary>\n\tpublic struct Seed\n\t{\n\t\tpublic string Tag;\n\t\tpublic int N;\n\t\tpublic int Genre;\n\t\tpublic string Vibe;\n\n\t\tpublic bool GenrePinned => Genre >= 0;\n\t\tpublic bool VibePinned => Vibe != null;\n\t}\n\n\t/// <summary>The station a tag names: trimmed and lower-cased, so \"Gamah\" and \" gamah \" are one\n\t/// station rather than three, with the default for an empty tag.</summary>\n\t/// <remarks>Every stream name in the toy is built from this, on BOTH targets, because the\n\t/// fallback word is load-bearing: it is part of what song a seed with no tag resolves to, and\n\t/// a host that picks its own makes <c>:23</c> a different song there than everywhere else. It\n\t/// has been exactly that \u2014 the s&amp;box player spelled the fallback \"skafinity\" while the\n\t/// engine and the web spelled it \"rotaliate\".</remarks>\n\tpublic static string Station( string tag ) =>\n\t\tstring.IsNullOrWhiteSpace( tag ) ? \"rotaliate\" : tag.Trim().ToLowerInvariant();\n\n\t/// <summary>The PRNG stream song <paramref name=\"n\"/> is COMPOSED from \u2014 what a host hands\n\t/// <see cref=\"MusicGen.Generate\"/>/<see cref=\"MusicGen.BeginPlan\"/> as the tag.</summary>\n\tpublic static string SongSeed( string tag, int n ) => $\"{Station( tag )}:{n}\";\n\n\t/// <summary>The stream song <paramref name=\"n\"/>'s VIBE is rolled from.</summary>\n\tpublic static string VibeSeed( string tag, int n ) => $\"{Station( tag )}:vibe:{n}\";\n\n\t/// <summary>The stream song <paramref name=\"n\"/>'s GENRE is rolled from. Separate from\n\t/// <see cref=\"VibeSeed\"/> on purpose: pinning the vibe must not change which genres a station\n\t/// plays, and pinning the genre must not change which vibes it rolls.</summary>\n\tpublic static string GenreSeed( string tag, int n ) => $\"{Station( tag )}:genre:{n}\";\n\n\t/// <summary>Song <paramref name=\"n\"/>'s rolled vibe \u2014 the same string on any machine, in any\n\t/// player, forever. This is what lets an endless line still BE its seed: nothing has to be\n\t/// remembered for Prev to replay exactly what was heard.</summary>\n\tpublic static string RollVibeFor( string tag, int n )\n\t{\n\t\tvar rng = new Rng( VibeSeed( tag, n ) );\n\t\treturn VibeCodec.RollVibe( rng.Next );\n\t}\n\n\t/// <summary>Song <paramref name=\"n\"/>'s rolled genre.</summary>\n\tpublic static int RollGenreFor( string tag, int n )\n\t{\n\t\tvar rng = new Rng( GenreSeed( tag, n ) );\n\t\treturn VibeCodec.RollGenre( rng.Next );\n\t}\n\n\t/// <summary>\n\t/// The station at position <paramref name=\"p\"/> of a SHUFFLED line \u2014 a player that answers each\n\t/// \"next\" with a whole new station rather than the next song of this one.\n\t///\n\t/// It is DERIVED from the root rather than drawn fresh, and that is the whole design: a random\n\t/// tag per song would make the line unrepeatable, so Prev could only work by remembering every\n\t/// station visited, and a reload would lose the lot. Derived, the shuffled line is still just a\n\t/// seed \u2014 walkable in both directions, the same everywhere, and reproducible from one string.\n\t///\n\t/// Position 0 is the root itself, so a pasted seed plays the song it names before the shuffle\n\t/// takes over.\n\t/// </summary>\n\tpublic static string RollTagFor( string root, int p )\n\t{\n\t\tif ( p <= 0 ) return root ?? \"\";\n\t\tvar rng = new Rng( $\"{Station( root )}:tag:{p}\" );\n\t\tvar sb = new StringBuilder( 8 );\n\t\tfor ( int i = 0; i < 8; i++ )\n\t\t{\n\t\t\tint q = Math.Clamp( (int)(rng.Next() * 36), 0, 35 );\n\t\t\tsb.Append( q < 10 ? (char)('0' + q) : (char)('a' + q - 10) );\n\t\t}\n\t\treturn sb.ToString();\n\t}\n\n\t// \u2500\u2500 Parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\tstatic bool IsTagChar( char ch ) =>\n\t\t(ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9')\n\t\t|| ch == '_' || ch == '-';\n\n\tstatic bool IsHex( string s )\n\t{\n\t\tforeach ( var ch in s )\n\t\t\tif ( VibeCodec.Hex.IndexOf( char.ToLowerInvariant( ch ) ) < 0 ) return false;\n\t\treturn true;\n\t}\n\n\t/// <summary>Parse a seed string. On failure <paramref name=\"error\"/> is a sentence fit to show\n\t/// a listener under the seed box, and <paramref name=\"seed\"/> is left at its default \u2014 there is\n\t/// no partial success, because half a seed is a song nobody asked for.</summary>\n\tpublic static bool TryParse( string s, out Seed seed, out string error )\n\t{\n\t\tseed = default;\n\t\terror = null;\n\t\ts = (s ?? \"\").Trim();\n\t\tif ( s.Length == 0 ) { error = \"a seed looks like tag:n\"; return false; }\n\n\t\tvar parts = s.Split( ':' );\n\t\tif ( parts.Length > 4 ) { error = \"too many parts \u2014 tag:n[:genre][:vibe]\"; return false; }\n\n\t\tforeach ( var ch in parts[0] )\n\t\t\tif ( !IsTagChar( ch ) )\n\t\t\t{\n\t\t\t\terror = $\"'{ch}' is not allowed in a station name (letters, digits, _ and - only)\";\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\tint n = 0;\n\t\tif ( parts.Length >= 2 )\n\t\t{\n\t\t\tif ( parts[1].Length == 0 ) { error = \"the song number is missing\"; return false; }\n\t\t\tforeach ( var ch in parts[1] )\n\t\t\t\tif ( ch < '0' || ch > '9' ) { error = $\"'{parts[1]}' is not a song number\"; return false; }\n\t\t\tif ( !int.TryParse( parts[1], out n ) ) { error = \"that song number is too big\"; return false; }\n\t\t}\n\n\t\tint genre = RolledGenre;\n\t\tstring vibe = null;\n\t\tfor ( int i = 2; i < parts.Length; i++ )\n\t\t{\n\t\t\tvar p = parts[i];\n\t\t\tif ( p.Length == 0 ) { error = \"an empty part \u2014 drop the extra ':'\"; return false; }\n\t\t\tif ( !IsHex( p ) ) { error = $\"'{p}' is not hex (0-9, a-f)\"; return false; }\n\t\t\tif ( p.Length == 1 )\n\t\t\t{\n\t\t\t\tif ( genre != RolledGenre ) { error = \"two genres in one seed\"; return false; }\n\t\t\t\tint g = VibeCodec.Hex.IndexOf( char.ToLowerInvariant( p[0] ) );\n\t\t\t\tif ( g >= VibeCodec.GenreCount )\n\t\t\t\t{\n\t\t\t\t\terror = $\"there is no genre '{p}' (0-{VibeCodec.Hex[VibeCodec.GenreCount - 1]})\";\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tgenre = g;\n\t\t\t}\n\t\t\telse if ( p.Length == VibeCodec.VibeLength )\n\t\t\t{\n\t\t\t\tif ( vibe != null ) { error = \"two vibes in one seed\"; return false; }\n\t\t\t\tvibe = p.ToLowerInvariant();\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\terror = $\"a vibe is {VibeCodec.VibeLength} characters, not {p.Length}\";\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\tseed = new Seed { Tag = parts[0], N = n, Genre = genre, Vibe = vibe };\n\t\treturn true;\n\t}\n\n\t/// <summary>The canonical string for a seed: pinned parts written genre-then-vibe, rolled parts\n\t/// left out. Round-trips through <see cref=\"TryParse\"/>.</summary>\n\tpublic static string Format( Seed seed )\n\t{\n\t\tvar sb = new StringBuilder();\n\t\tsb.Append( seed.Tag ?? \"\" ).Append( ':' ).Append( Math.Max( 0, seed.N ) );\n\t\tif ( seed.GenrePinned ) sb.Append( ':' ).Append( VibeCodec.Hex[Math.Clamp( seed.Genre, 0, VibeCodec.GenreCount - 1 )] );\n\t\tif ( seed.VibePinned ) sb.Append( ':' ).Append( seed.Vibe );\n\t\treturn sb.ToString();\n\t}\n\n\t/// <summary>The same seed with everything it left to chance written down \u2014 what \"copy this\n\t/// song\" hands over, as opposed to \"copy this station\", which is <see cref=\"Format\"/> of the\n\t/// seed as it stands.</summary>\n\tpublic static Seed Resolved( Seed seed )\n\t{\n\t\tif ( !seed.GenrePinned ) seed.Genre = RollGenreFor( seed.Tag, seed.N );\n\t\tif ( !seed.VibePinned ) seed.Vibe = RollVibeFor( seed.Tag, seed.N );\n\t\treturn seed;\n\t}\n\n\t/// <summary>Put a seed's song onto <paramref name=\"c\"/>: its genre and its 36 knobs, pinned or\n\t/// rolled. Per-instrument volumes are NOT touched \u2014 they are a local mix preference, so a host\n\t/// overlays its own after this (<see cref=\"VibeCodec.ApplyVolumes\"/>).</summary>\n\tpublic static void Apply( Seed seed, MusicGen.Config c )\n\t{\n\t\tif ( c == null ) return;\n\t\tvar r = Resolved( seed );\n\t\tc.Genre = Math.Clamp( r.Genre, 0, VibeCodec.GenreCount - 1 );\n\t\tVibeCodec.Apply( r.Vibe, c );\n\t}\n}\n"
        },
        {
            "Ident": "gamah.skafinity",
            "Path": "Engine/Wav.cs",
            "FileName": "Wav.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 341414,
            "IsPrivate": false,
            "Code": "using System;\nusing System.Collections.Generic;\n\nnamespace Skafinity;\n\n/// <summary>\n/// The WAV container \u2014 16-bit PCM, the one format both targets can hand straight to a player.\n/// Stateless: it wraps samples somebody else rendered.\n/// </summary>\nstatic class Wav\n{\n\t/// <summary>Clamp a \u22121..1 mix sample to signed 16-bit.</summary>\n\tpublic static short ToS16( float v ) => (short)(Math.Clamp( v, -1f, 1f ) * 32767f);\n\n\t/// <summary>Wrap already-rendered 16-bit samples in a WAV. Mono or interleaved stereo per\n\t/// <paramref name=\"channels\"/>.</summary>\n\tpublic static byte[] FromSamples( short[] samples, int channels, int sampleRate )\n\t{\n\t\tint dataSize = samples.Length * 2;\n\t\tint blockAlign = channels * 2;\n\t\tvar bytes = new List<byte>( 44 + dataSize );\n\t\tvoid Str( string s ) { foreach ( var ch in s ) bytes.Add( (byte)ch ); }\n\t\tvoid U32( uint v ) { bytes.Add( (byte)v ); bytes.Add( (byte)(v >> 8) ); bytes.Add( (byte)(v >> 16) ); bytes.Add( (byte)(v >> 24) ); }\n\t\tvoid U16( ushort v ) { bytes.Add( (byte)v ); bytes.Add( (byte)(v >> 8) ); }\n\t\tStr( \"RIFF\" ); U32( (uint)(36 + dataSize) ); Str( \"WAVE\" );\n\t\tStr( \"fmt \" ); U32( 16 ); U16( 1 ); U16( (ushort)channels );\n\t\tU32( (uint)sampleRate ); U32( (uint)(sampleRate * blockAlign) ); U16( (ushort)blockAlign ); U16( 16 );\n\t\tStr( \"data\" ); U32( (uint)dataSize );\n\t\tforeach ( var s in samples ) { ushort u = (ushort)s; bytes.Add( (byte)u ); bytes.Add( (byte)(u >> 8) ); }\n\t\treturn bytes.ToArray();\n\t}\n}\n\npublic sealed partial class MusicGen\n{\n\t// \u2500\u2500 Output \u2500\u2500\n\tshort[] ToShorts( float gain )\n\t{\n\t\tint n = _bufL.Length;\n\t\tvar s = new short[n * Channels];\n\t\tfor ( int i = 0; i < n; i++ )\n\t\t{\n\t\t\ts[i * 2] = Wav.ToS16( _bufL[i] * gain );\n\t\t\ts[i * 2 + 1] = Wav.ToS16( _bufR[i] * gain );\n\t\t}\n\t\treturn s;\n\t}\n\n\t/// <summary>Wrap already-rendered 16-bit samples in a WAV (for export). Mono or\n\t/// interleaved stereo per <paramref name=\"channels\"/>.</summary>\n\tpublic static byte[] WavFromSamples( short[] samples, int channels, int sampleRate )\n\t\t=> Wav.FromSamples( samples, channels, sampleRate );\n\n\tbyte[] EncodeWav( float gain ) => Wav.FromSamples( ToShorts( gain ), Channels, _sr );\n}\n"
        },
        {
            "Ident": "gamah.skafinity",
            "Path": "UI/SkafinityBoard.cs",
            "FileName": "SkafinityBoard.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 341414,
            "IsPrivate": false,
            "Code": "using System;\nusing System.Collections.Generic;\n\nnamespace Skafinity;\n\n/// <summary>\n/// The board's PRESENTATION RULES, with no widget toolkit in sight: what the controls are called,\n/// what they say when they change, how a length or a playlist row is worded, and how the vibe's\n/// fields fall into a grid. Everything here is a pure function of engine state.\n/// </summary>\n/// <remarks>\n/// <para>This exists because the same board is drawn twice \u2014 once as a Razor panel here, once as\n/// <c>web/skafinity-element.js</c> \u2014 and the half that drifts between two drawings of one design is\n/// never the layout, it is the wording and the small derived decisions: which button is disabled,\n/// what a row says when nothing is cached, whether a knob repeats its column's name. Those are\n/// written down once, here.</para>\n///\n/// <para><b>Framework-free on purpose, and not yet shared.</b> Nothing in this file touches\n/// <c>Sandbox.*</c>, so it can move under <c>Code/Engine/</c> \u2014 the folder both targets compile \u2014\n/// the day the web asks the wasm for it instead of keeping its own copy. It is deliberately NOT\n/// there yet: a file under <c>Engine/</c> is a file in the wasm bundle, and adding one costs a full\n/// AOT re-stage (see the stale-bundle gate in CLAUDE.md) for code nothing on that side calls yet.\n/// Keep it free of engine-target-hostile types so that move stays a move rather than a rewrite.</para>\n/// </remarks>\npublic static class SkafinityBoard\n{\n\t// \u2500\u2500 The grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\t/// <summary>One header per vibe-matrix column. Column 0 (VOLUME) is a local mix preference and\n\t/// never travels; columns 1..4 are the wire. The grid is rectangular, so a voice with nothing in\n\t/// its last column simply leaves that cell empty.</summary>\n\tpublic static readonly string[] ColumnHeaders = { \"VOLUME\", \"TONE\", \"CHARACTER\", \"EXTRA\", \"MORE\" };\n\n\t/// <summary>One row of the per-instrument mixer: a voice and its cells, one per\n\t/// <see cref=\"ColumnHeaders\"/> entry, null where this genre leaves a column empty.</summary>\n\tpublic readonly struct MatrixRow\n\t{\n\t\t/// <summary>Voice name \u2014 the row label (BASS, DRUMS, \u2026).</summary>\n\t\tpublic string Voice { get; init; }\n\t\t/// <summary>Cells by column index; null = this genre has no knob there.</summary>\n\t\tpublic VibeCodec.Field[] Cells { get; init; }\n\t}\n\n\t/// <summary>Lay the genre's vibe fields out as the mixer grid: one row per voice, in the\n\t/// library's own display order. Fields with no voice are GLOBAL and come back from\n\t/// <see cref=\"Globals\"/> instead.</summary>\n\t/// <remarks>Driven entirely from the field metadata, so a new genre \u2014 or a new knob \u2014 is a pure\n\t/// engine change and there is no field table in any UI.</remarks>\n\tpublic static List<MatrixRow> Matrix( int genre )\n\t{\n\t\tvar order = new List<string>();\n\t\tvar byVoice = new Dictionary<string, VibeCodec.Field[]>();\n\t\tforeach ( var f in VibeCodec.Fields( genre ) )\n\t\t{\n\t\t\tif ( f.Voice == null ) continue;\n\t\t\tif ( !byVoice.TryGetValue( f.Voice, out var cells ) )\n\t\t\t{\n\t\t\t\tcells = new VibeCodec.Field[ColumnHeaders.Length];\n\t\t\t\tbyVoice[f.Voice] = cells;\n\t\t\t\torder.Add( f.Voice );\n\t\t\t}\n\t\t\tif ( f.Column >= 0 && f.Column < cells.Length ) cells[f.Column] = f;\n\t\t}\n\t\tvar rows = new List<MatrixRow>( order.Count );\n\t\tforeach ( var v in order ) rows.Add( new MatrixRow { Voice = v, Cells = byVoice[v] } );\n\t\treturn rows;\n\t}\n\n\t/// <summary>The genre's knobs that belong to no instrument \u2014 the GLOBAL strip under the grid.\n\t/// Often empty (the globals have been retired to reserved wire slots), and a heading over an\n\t/// empty grid reads as a panel that failed to draw something, so callers check.</summary>\n\tpublic static List<VibeCodec.Field> Globals( int genre )\n\t{\n\t\tvar list = new List<VibeCodec.Field>();\n\t\tforeach ( var f in VibeCodec.Fields( genre ) )\n\t\t\tif ( f.Voice == null ) list.Add( f );\n\t\treturn list;\n\t}\n\n\t/// <summary>What to write above a knob in the grid: nothing when the column header already says\n\t/// it (VOLUME under VOLUME reads as a mistake), the field's own name otherwise.</summary>\n\tpublic static string KnobLabel( VibeCodec.Field f, int column ) =>\n\t\tf == null ? \"\" : f.Name == ColumnHeaders[column] ? \"\" : f.Name;\n\n\t/// <summary>Index of a field within its genre's field list \u2014 what\n\t/// <see cref=\"SkafinityPlayer.SetVibe\"/> takes.</summary>\n\tpublic static int FieldIndex( int genre, VibeCodec.Field field )\n\t{\n\t\tvar fields = VibeCodec.Fields( genre );\n\t\tfor ( int i = 0; i < fields.Count; i++ )\n\t\t\tif ( ReferenceEquals( fields[i], field ) ) return i;\n\t\treturn -1;\n\t}\n\n\t/// <summary>Which of a choice field's options a 0..1 value selects.</summary>\n\tpublic static int ChoiceIndex( VibeCodec.Field f, float norm ) =>\n\t\tf?.Choices == null ? 0\n\t\t: Math.Clamp( (int)MathF.Round( norm * (f.Choices.Length - 1) ), 0, f.Choices.Length - 1 );\n\n\t/// <summary>\u2026and the 0..1 value that selects option <paramref name=\"k\"/>.</summary>\n\tpublic static float ChoiceNorm( VibeCodec.Field f, int k ) =>\n\t\tf?.Choices == null || f.Choices.Length < 2 ? 0f : k / (float)(f.Choices.Length - 1);\n\n\t// \u2500\u2500 Numbers as words \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\t/// <summary>Shown where a length would be if there were one. A song that has not been rendered\n\t/// has no length to state, and an honest dash beats 0:00 \u2014 which reads as a song of no length.</summary>\n\tpublic const string NoTime = \"\u2013:\u2013\u2013\";\n\n\t/// <summary>m:ss, or <see cref=\"NoTime\"/> when the length is not known yet.</summary>\n\tpublic static string Time( double seconds, bool known = true )\n\t{\n\t\tif ( !known || double.IsNaN( seconds ) ) return NoTime;\n\t\tint t = (int)Math.Round( Math.Max( 0, seconds ) );\n\t\treturn $\"{t / 60}:{(t % 60):00}\";\n\t}\n\n\t/// <summary>A 0..1 fraction as a CSS width.</summary>\n\tpublic static string Percent( float f ) => $\"{(int)MathF.Round( Math.Clamp( f, 0f, 1f ) * 100 )}%\";\n\n\t/// <summary>Genre name for an id, or \"?\" \u2014 a UI drawing a row for a genre the engine does not\n\t/// have should show that rather than throw.</summary>\n\tpublic static string GenreName( int g ) =>\n\t\tg >= 0 && g < VibeCodec.GenreCount ? VibeCodec.Genres[g] : \"?\";\n\n\t/// <summary>The caret column of a playlist row: on the song you are hearing, nothing otherwise.\n\t/// A column rather than a prefix so every row's number starts at the same x.</summary>\n\tpublic static string RowCaret( SkafinityPlayer.QueueEntry e ) => e.Current ? \"\u25b6\" : \"\";\n\n\t/// <summary>What a playlist row says on its right-hand side. Generating rows draw a bar instead\n\t/// and never reach this.</summary>\n\tpublic static string RowStatus( SkafinityPlayer.QueueEntry e ) =>\n\t\te.Current ? Copy.RowNow : e.Cached ? Copy.RowReady : e.Past ? Copy.RowGone : Copy.RowPending;\n\n\t// \u2500\u2500 The words \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\t/// <summary>Every user-visible string on the board, in one place. The tooltips carry the reason a\n\t/// control exists, which is the part that is genuinely hard to reconstruct \u2014 \"reroll\" and\n\t/// \"randomize\" are both dice, and only their tooltips say why there are two.</summary>\n\tpublic static class Copy\n\t{\n\t\t// Transport\n\t\tpublic const string Prev = \"\u23ee\";\n\t\tpublic const string PrevTitle = \"Previous song\";\n\t\tpublic const string Play = \"\u25b6\";\n\t\tpublic const string Pause = \"\u23f8\";\n\t\tpublic const string PlayTitle = \"Play / Pause\";\n\t\tpublic const string Next = \"\u23ed\";\n\t\tpublic const string NextTitle = \"Next song\";\n\t\t/// <summary>Ends in the hash on purpose \u2014 see <see cref=\"Hash\"/>.</summary>\n\t\tpublic const string NowPlaying = \"now playing #\";\n\t\t/// <summary>A number sign, ALONE, as its own label.</summary>\n\t\t/// <remarks>A label whose text is LONGER than one character and begins with <c>#</c> is a\n\t\t/// localisation token: the engine looks the rest up as a phrase and renders what comes back,\n\t\t/// so <c>#24</c> silently becomes <c>24</c>. That is why no label here is built as \"#\" plus a\n\t\t/// number \u2014 the hash either ends the text before it, or stands alone in a panel of its own,\n\t\t/// where the length rule leaves it untouched.</remarks>\n\t\tpublic const string Hash = \"#\";\n\t\tpublic const string Volume = \"vol\";\n\t\tpublic const string SeekTitle = \"Seek within this song\";\n\t\t/// <summary>Playback is stalled on a song being rendered \u2014 as opposed to the silent\n\t\t/// background look-ahead, which nobody needs to be told about.</summary>\n\t\tpublic static string Generating( int n ) => $\"generating #{n}\u2026\";\n\n\t\t// Seed\n\t\tpublic const string SeedPlaceholder = \"tag:n[:genre][:vibe]\";\n\t\t/// <summary>Typing a seed and being handed a stopped transport is a dead end, so the button\n\t\t/// says play, because that is what it does.</summary>\n\t\tpublic const string SeedGo = \"play\";\n\t\tpublic const string CopySong = \"copy seed\";\n\t\tpublic const string CopySongTitle = \"Copy this song fully written down \u2014 the genre and the vibe spelled out, so it plays the same anywhere\";\n\t\tpublic const string CopyStation = \"copy station\";\n\t\tpublic const string CopyStationTitle = \"Copy the seed as it stands \u2014 whatever it leaves rolling keeps rolling\";\n\t\tpublic const string Copied = \"copied!\";\n\n\t\t// What plays\n\t\tpublic const string Genre = \"genre\";\n\t\tpublic const string GenreRandom = \"Random\";\n\t\tpublic const string Reroll = \"\ud83c\udfb2 reroll\";\n\t\tpublic const string RerollTitle = \"A fresh station at song 0 \u2014 anything you have pinned stays pinned\";\n\t\tpublic const string ShuffleOn = \"\ud83d\udd00 shuffle: ON\";\n\t\tpublic const string ShuffleOff = \"\ud83d\udd00 shuffle: OFF\";\n\t\tpublic const string ShuffleTitle = \"Every next song is a whole new station rather than the next song of this one\";\n\t\tpublic const string Tinker = \"\ud83c\udf9b tinker\";\n\t\tpublic const string TinkerOpen = \"hide knobs\";\n\n\t\t// The knobs\n\t\tpublic const string VibeHeading = \"vibe\";\n\t\tpublic const string GlobalHeading = \"GLOBAL\";\n\t\tpublic const string VibeRoll = \"\ud83c\udfb2 randomize\";\n\t\tpublic const string VibeRollTitle = \"Throw every knob somewhere new and keep it \u2014 the seed carries these values\";\n\t\tpublic const string VibeRandom = \"\u21ba random each song\";\n\t\tpublic const string VibeRandomTitle = \"Stop pinning these knobs \u2014 let every song roll its own again\";\n\n\t\t// The playlist\n\t\tpublic const string PlaylistHeading = \"playlist\";\n\t\tpublic const string JumpTo = \"jump to\";\n\t\tpublic const string JumpGo = \"go\";\n\t\tpublic const string RowNow = \"now\";\n\t\tpublic const string RowReady = \"ready\";\n\t\tpublic const string RowGone = \"gone\";\n\t\tpublic const string RowPending = \"\u2014\";\n\t\tpublic const string Export = \"\u2b07 .wav\";\n\t\tpublic const string ExportBusy = \"\u2b07 \u2026\";\n\t\tpublic static string ExportTitle( int n ) => $\"Export #{n}\";\n\n\t\t// What the message line says. A refused seed says so INLINE and changes nothing: a toast\n\t\t// that has faded is no help to somebody looking at a board that did nothing.\n\t\tpublic static string Saved( string file ) => $\"Saved {file} to your s&box data folder\";\n\t\tpublic const string SaveFailed = \"Couldn't save song\";\n\t\tpublic static string Playing( string seed ) => $\"Playing {seed}\";\n\t\tpublic const string NewStation = \"New station\";\n\t\tpublic const string VibeRolled = \"Threw every knob but the volumes, and pinned them\";\n\t\tpublic const string VibeUnpinned = \"Every song rolls its own vibe again \u2014 what changes is the songs after this one\";\n\t\tpublic const string GenreUnpinned = \"Every song rolls its own genre again\";\n\t}\n}\n"
        },
        {
            "Ident": "gamah.skafinity",
            "Path": "Code/Engine/Drums/Kit.cs",
            "FileName": "Kit.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 341414,
            "IsPrivate": false,
            "Code": "using System;\nusing System.Collections.Generic;\n\nusing static Skafinity.Osc;\n\nnamespace Skafinity;\n\n// The kit's voices: synthesised kick, snare, tom, hat, crash and ride.\n//\n// Each one returns immediately when the kit is muted (_drumGain 0) \u2014 it would write silence.\n// The guard sits INSIDE the voices rather than at the call site because the caller interleaves\n// pattern decisions with these calls: RenderDrumBar draws noise.Chance() to pick tom-vs-ghost\n// and RenderFill draws rng.Chance() between hits, so skipping a call would move the stream.\n// The per-voice `noise` draws being skipped are local \u2014 `noise` is a fresh per-block Rng, and\n// when the kit is muted nothing downstream reads it.\n//\n// EVERY VOICE IS PARAMETERISED BY A TONE STRUCT, and every struct's Default reproduces the\n// numbers the groove path has always played. A candidate tuning is an argument, not an edit, so\n// the audition diagnostic can sweep one without the grooves hearing about it \u2014 which is what\n// lets a kit be chosen by listening rather than by rebuilding between takes.\n//\n// Part of the MusicGen engine \u2014 see MusicGen.cs.\n\n/// <summary>Two-pole band-pass (Chamberlin SVF), for the cymbal voices' resonant clusters. The\n/// synth's own SVF is inline in the pitched render loop (Synth/Render.cs) and is not reachable\n/// from here; this is the same filter, kept next to the voices that use it.</summary>\nstruct BandPass\n{\n\tfloat _low, _band;\n\treadonly float _f, _q;\n\n\tpublic BandPass( float fc, float q, int sr )\n\t{\n\t\t_low = 0f; _band = 0f;\n\t\t// Clamped well under Nyquist: the Chamberlin form goes unstable as f approaches 2.\n\t\t_f = (float)(2 * Math.Sin( Math.PI * Math.Min( fc, sr * 0.15f ) / sr ));\n\t\t_q = Math.Clamp( q, 0.004f, 2f );\n\t}\n\n\t/// <summary>NORMALISED BY Q. A resonant band-pass has a centre gain of about 1/Q, so a\n\t/// ringing resonance is ~100\u00d7 louder than a gentle one for the same input \u2014 which makes the\n\t/// Q a level control as well as a bandwidth, and there is no setting of the two that is then\n\t/// independently correct. Scaling by Q on the way out separates them again: how tight the\n\t/// resonance is, and how loud it is, become two numbers.</summary>\n\tpublic float Next( float x )\n\t{\n\t\tfloat high = x - _low - _q * _band;\n\t\t_band += _f * high;\n\t\t_low += _f * _band;\n\t\treturn _band * _q;\n\t}\n}\n\n/// <summary>\n/// WHAT THE AUDITION APPROVED, and it is mostly RANGES rather than values.\n///\n/// Several of round 2's questions came back \"all of these work\" \u2014 the click's corner at 1.8, 3.5\n/// and 6 kHz; the rimshot's crack at 2.4, 3.2 and 4.2 kHz; both cross-sticks; both foot chicks.\n/// That is not an undecided answer. A kit is a physical object being hit by a person, and the\n/// same drum does not make the identical sound twice; a band of values that all read as the\n/// right drum is exactly what NUANCE is, and picking one point out of it by ear would be\n/// throwing the finding away. Phase 2 draws from these per song (and per hit where it says so),\n/// which is the same reason RenderKick's round-robin jitter exists.\n///\n/// NOTHING HERE IS WIRED IN YET. The tone structs' Defaults are still what the grooves play, so\n/// the render digests are untouched and Phase 1 stays provably pure. These are the numbers\n/// Phase 2 wires, and they are recorded here rather than in a comment so they cannot drift.\n/// </summary>\nstatic class KitNuance\n{\n\t/// <summary>Kick click low-pass corner. Full-band was the high tick on every body.</summary>\n\tpublic const float ClickCutMin = 1800f, ClickCutMax = 6000f;\n\n\t/// <summary>Rimshot crack centre. Darker reads as a bigger drum, brighter as a harder hit \u2014\n\t/// both are the same articulation, so this is per HIT, not per song.</summary>\n\tpublic const float RimCrackMin = 2400f, RimCrackMax = 4200f;\n\n\t/// <summary>Cross-stick, between the two that were kept: the higher/thinner knock and the\n\t/// lower/thicker one. Crack centre and thud level move together.</summary>\n\tpublic const float StickCrackMin = 1350f, StickCrackMax = 1750f;\n\tpublic const float StickThudMin = 0.34f, StickThudMax = 0.50f;\n\n\t/// <summary>Foot chick, between the two that were kept: the tighter, brighter chick and the\n\t/// slower, darker one. All three move together \u2014 a foot that closes slower is duller and\n\t/// longer, because it is the same motion done differently.</summary>\n\tpublic const float FootAttackMin = 0.011f, FootAttackMax = 0.022f;\n\tpublic const float FootDurMin = 0.085f, FootDurMax = 0.115f;\n\tpublic const float FootCutMin = 2600f, FootCutMax = 3400f;\n\n\t/// <summary>The open hi-hat's ring, and its corner. Approved as a range for the same reason\n\t/// the others are: the tail of an open hat is how hard the foot was off it, which is a\n\t/// different amount every bar. A hat that rings the identical 600 ms eight times is the same\n\t/// tell as a kick that never varies.\n\t///\n\t/// <para>The band was 0.45\u20130.75 s, and that is a hat measured on its own rather than one in a\n\t/// pattern. With <c>decayFrac</c> ~0.45 it is a 200\u2013340 ms time constant, while an eighth at\n\t/// the top of a genre's band is ~185 ms \u2014 so the open hat was still half up when the next\n\t/// stroke landed, on every open cell, and a groove that plays them continuously (the country\n\t/// train beat) reads as a wash that never clears rather than as an open hat. The range is kept\n\t/// because the nuance is real; it is the LENGTH that was a solo measurement.</para>\n\t///\n\t/// <para>THE VALUE HERE IS THE ONE THAT COUNTS: <c>HatTone.Default.openDur</c> is overridden\n\t/// per song from this band (see Compose.cs), so editing the preset moves nothing a listener\n\t/// hears \u2014 the digests not budging is what says so.</para></summary>\n\tpublic const float OpenHatDurMin = 0.26f, OpenHatDurMax = 0.42f;\n\tpublic const float OpenHatCut = 6250f;\n\n\t/// <summary>Where half open sits. The pedal's travel is geometric (see RenderHat), and this is\n\t/// the exponent on it: 1 is a straight ratio sweep, below 1 opens the low end of the travel\n\t/// sooner. A steady lift has to sound steady, which is the thing this number is set against \u2014\n\t/// a single half-open hat cannot tell you whether the map is even, only whether one point on\n\t/// it is pleasant.</summary>\n\tpublic const float HatOpenCurve = 0.50f;\n\n\t/// <summary>A LIFT LANDS ON A CHOKE. The foot comes down on the downbeat and whatever is\n\t/// ringing stops; that is the event, and it reads correctly whether or not a stick lands with\n\t/// it. What does NOT work is a foot chick as the landing \u2014 the chick is its own articulation\n\t/// (it is the hat speaking on its own, on 2 and 4, under silence) and it has nothing to say\n\t/// at the end of a phrase that a choke has not already said.</summary>\n\t/// <summary>THE CYMBALS' BANDS. Carried over from the audition rounds that swept them on the\n\t/// mode-forest cymbal: they are parameters of the same three laws \u2014 how much splash, how much\n\t/// wash, how long the ring, where the bell's clang sits \u2014 so they transfer, but they were\n\t/// approved on a different spelling of those laws and want a fresh listen. Only what was\n\t/// actually swept is a band: the bell's splash and the bright crash's wash were never varied in\n\t/// front of a listener and stay at 1.</summary>\n\tpublic const float RideSplashMin = 0.5f, RideSplashMax = 1.8f;\n\tpublic const float RideWashMin = 1.0f, RideWashMax = 1.8f;\n\tpublic const float RideRingMin = 1.0f, RideRingMax = 1.4f;\n\tpublic const float BellClangMin = 2000f, BellClangMax = 2600f;\n\tpublic const float BellRingMin = 1.0f, BellRingMax = 1.4f;\n\tpublic const float CrashSplashMin = 0.5f, CrashSplashMax = 1.6f;\n\tpublic const float CrashRingMin = 0.7f, CrashRingMax = 1.4f;\n\tpublic const float DarkSplashMin = 0.5f, DarkSplashMax = 1.0f;\n\tpublic const float DarkWashMin = 1.0f, DarkWashMax = 1.6f;\n\tpublic const float DarkRingMin = 1.0f, DarkRingMax = 1.3f;\n\n\t/// <summary>Interpolate a kit nuance. <paramref name=\"u\"/> is 0..1 \u2014 a per-song or per-hit\n\t/// draw. One helper so a nuance is always read the same way.</summary>\n\tpublic static float At( float min, float max, float u )\n\t\t=> min + (max - min) * Math.Clamp( u, 0f, 1f );\n}\n\n// \u2500\u2500 The tone structs \u2500\u2500\n\n/// <summary>The kick's body. Defaults are what the grooves have always played.</summary>\nreadonly struct KickTone\n{\n\tpublic readonly float Dur;          // seconds\n\tpublic readonly double DecayFrac;   // decay time as a fraction of Dur\n\tpublic readonly float StartHz;      // pitch at the attack\n\tpublic readonly float DropHz;       // how far the pitch falls\n\tpublic readonly float DropRate;     // how fast it falls (in units of 1/Dur)\n\tpublic readonly float Drive;        // tanh drive on the body\n\tpublic readonly float SubHz;\n\tpublic readonly float SubLevel;\n\tpublic readonly double SubDecayFrac;\n\tpublic readonly float ClickLevel;\n\tpublic readonly float ClickSec;\n\t/// <summary>Low-pass corner on the click. 0 leaves it full-band \u2014 which is white noise, and\n\t/// reads as a high tick sitting on top of the drum rather than as a beater hitting a head.\n\t/// A beater is a soft mass on a skin: the attack it makes is mid-band.</summary>\n\tpublic readonly float ClickCut;\n\tpublic readonly float Beater;       // level of the beater-return transient (0 = none)\n\tpublic readonly float BeaterSec;    // how long after the hit the beater comes back\n\tpublic readonly float Jitter;       // round-robin variation depth, 0..1\n\n\tpublic KickTone( float dur, double decayFrac, float startHz, float dropHz, float dropRate,\n\t\tfloat drive, float subHz, float subLevel, double subDecayFrac, float clickLevel,\n\t\tfloat clickSec, float clickCut, float beater, float beaterSec, float jitter )\n\t{\n\t\tDur = dur; DecayFrac = decayFrac; StartHz = startHz; DropHz = dropHz; DropRate = dropRate;\n\t\tDrive = drive; SubHz = subHz; SubLevel = subLevel; SubDecayFrac = subDecayFrac;\n\t\tClickLevel = clickLevel; ClickSec = clickSec; ClickCut = clickCut;\n\t\tBeater = beater; BeaterSec = beaterSec;\n\t\tJitter = jitter;\n\t}\n\n\tpublic KickTone With( float dur = -1f, double decayFrac = -1.0, float startHz = -1f,\n\t\tfloat dropHz = -1f, float dropRate = -1f, float drive = -1f, float subHz = -1f,\n\t\tfloat subLevel = -1f, double subDecayFrac = -1.0, float clickLevel = -1f,\n\t\tfloat clickSec = -1f, float clickCut = -1f, float beater = -1f, float jitter = -1f )\n\t\t=> new( dur < 0 ? Dur : dur, decayFrac < 0 ? DecayFrac : decayFrac,\n\t\t\tstartHz < 0 ? StartHz : startHz, dropHz < 0 ? DropHz : dropHz,\n\t\t\tdropRate < 0 ? DropRate : dropRate, drive < 0 ? Drive : drive,\n\t\t\tsubHz < 0 ? SubHz : subHz, subLevel < 0 ? SubLevel : subLevel,\n\t\t\tsubDecayFrac < 0 ? SubDecayFrac : subDecayFrac,\n\t\t\tclickLevel < 0 ? ClickLevel : clickLevel, clickSec < 0 ? ClickSec : clickSec,\n\t\t\tclickCut < 0 ? ClickCut : clickCut,\n\t\t\tbeater < 0 ? Beater : beater, BeaterSec, jitter < 0 ? Jitter : jitter );\n\n\tpublic static readonly KickTone Default = new(\n\t\tdur: 0.17f, decayFrac: 0.31, startHz: 127f, dropHz: 80f, dropRate: 2.6f, drive: 1.6f,\n\t\tsubHz: 44f, subLevel: 0.3f, subDecayFrac: 0.55, clickLevel: 0.55f, clickSec: 0.003f,\n\t\tclickCut: 0f, beater: 0f, beaterSec: 0.023f, jitter: 0f );\n}\n\n/// <summary>How the snare is struck. The single-hit articulations are tone presets; the flam and\n/// the buzz are gestures made of several hits and have their own entry points.</summary>\nenum SnareHit { Hit, Ghost, Rimshot, CrossStick, SnaresOff }\n\nreadonly struct SnareTone\n{\n\tpublic readonly float Dur;\n\tpublic readonly double DecayFrac;\n\tpublic readonly float Hz1, Hz2;\n\tpublic readonly float Body2;      // level of the second shell partial\n\tpublic readonly float BodyLevel;\n\tpublic readonly float Sag;        // how far the shell pitch falls over the hit\n\tpublic readonly float Wire;       // amount of snare-wire noise\n\tpublic readonly float WireCut;    // wire high-pass corner\n\tpublic readonly float WireDrive;\n\t/// <summary>How hard this articulation is struck, relative to a plain backbeat.</summary>\n\tpublic readonly float Level;\n\t/// <summary>THE CRACK: a tight, fast band of noise at the attack \u2014 stick on rim, wood on wood.\n\t/// It is what a rimshot and a cross-stick actually ARE. Reaching for them with the shell\n\t/// partials instead is what makes a rimshot ring like a tom and a cross-stick read as a clave:\n\t/// two loud sines with a slow decay are a pitched percussion instrument, whatever they are\n\t/// labelled. 0 = no crack, which is the plain backbeat.</summary>\n\tpublic readonly float CrackHz, CrackQ, CrackLevel;\n\tpublic readonly double CrackDecayFrac;\n\t/// <summary>A little low knock under the crack \u2014 the shell moving. Not a tone.</summary>\n\tpublic readonly float ThudHz, ThudLevel;\n\n\tpublic SnareTone( float dur, double decayFrac, float hz1, float hz2, float body2,\n\t\tfloat bodyLevel, float sag, float wire, float wireCut, float wireDrive, float level,\n\t\tfloat crackHz = 0f, float crackQ = 0.35f, float crackLevel = 0f,\n\t\tdouble crackDecayFrac = 0.10, float thudHz = 0f, float thudLevel = 0f )\n\t{\n\t\tDur = dur; DecayFrac = decayFrac; Hz1 = hz1; Hz2 = hz2; Body2 = body2;\n\t\tBodyLevel = bodyLevel; Sag = sag; Wire = wire; WireCut = wireCut; WireDrive = wireDrive;\n\t\tLevel = level;\n\t\tCrackHz = crackHz; CrackQ = crackQ; CrackLevel = crackLevel;\n\t\tCrackDecayFrac = crackDecayFrac; ThudHz = thudHz; ThudLevel = thudLevel;\n\t}\n\n\tpublic SnareTone With( float dur = -1f, double decayFrac = -1.0, float hz1 = -1f, float hz2 = -1f,\n\t\tfloat bodyLevel = -1f, float sag = -1f, float wire = -1f, float wireCut = -1f,\n\t\tfloat level = -1f, float crackHz = -1f, float crackLevel = -1f, float thudLevel = -1f )\n\t\t=> new( dur < 0 ? Dur : dur, decayFrac < 0 ? DecayFrac : decayFrac,\n\t\t\thz1 < 0 ? Hz1 : hz1, hz2 < 0 ? Hz2 : hz2, Body2,\n\t\t\tbodyLevel < 0 ? BodyLevel : bodyLevel, sag < 0 ? Sag : sag,\n\t\t\twire < 0 ? Wire : wire, wireCut < 0 ? WireCut : wireCut, WireDrive,\n\t\t\tlevel < 0 ? Level : level, crackHz < 0 ? CrackHz : crackHz, CrackQ,\n\t\t\tcrackLevel < 0 ? CrackLevel : crackLevel, CrackDecayFrac, ThudHz,\n\t\t\tthudLevel < 0 ? ThudLevel : thudLevel );\n\n\tpublic static readonly SnareTone Default = new(\n\t\tdur: 0.15f, decayFrac: 0.32, hz1: 185f, hz2: 268f, body2: 0.6f, bodyLevel: 0.375f,\n\t\tsag: 0.14f, wire: 0.6f, wireCut: 1350f, wireDrive: 1.2f, level: 1f );\n\n\t/// <summary>The ghost note \u2014 the groove path's `ghost: true`, byte for byte.</summary>\n\tpublic static readonly SnareTone Ghost = new(\n\t\tdur: 0.06f, decayFrac: 0.3, hz1: 185f, hz2: 268f, body2: 0.6f, bodyLevel: 0.375f,\n\t\tsag: 0.14f, wire: 0.6f, wireCut: 1350f, wireDrive: 1.2f, level: 0.3f );\n\n\t/// <summary>Stick on rim and head together: the shell speaks louder and higher, the wires\n\t/// crack harder, and the whole thing is shorter than a struck note.</summary>\n\tpublic static readonly SnareTone Rimshot = new(\n\t\tdur: 0.14f, decayFrac: 0.20, hz1: 300f, hz2: 452f, body2: 0.5f, bodyLevel: 0.13f,\n\t\tsag: 0.26f, wire: 0.95f, wireCut: 1900f, wireDrive: 2.6f, level: 1.25f,\n\t\tcrackHz: 3200f, crackQ: 0.55f, crackLevel: 2.2f, crackDecayFrac: 0.055 );\n\n\t/// <summary>Stick laid across the head, struck on the rim: a woody KNOCK. It is damped by the\n\t/// hand holding the stick down, so it does not ring \u2014 and it is the ringing, not the pitch,\n\t/// that makes a bright short tone read as a clave.</summary>\n\tpublic static readonly SnareTone CrossStick = new(\n\t\tdur: 0.05f, decayFrac: 0.085, hz1: 520f, hz2: 735f, body2: 0.45f, bodyLevel: 0.24f,\n\t\tsag: 0.30f, wire: 0.07f, wireCut: 2400f, wireDrive: 1.0f, level: 0.85f,\n\t\tcrackHz: 1750f, crackQ: 0.75f, crackLevel: 1.3f, crackDecayFrac: 0.10,\n\t\tthudHz: 155f, thudLevel: 0.34f );\n\n\t/// <summary>Wires thrown off: no crack, just the shell \u2014 which is what makes it read as a\n\t/// tom rather than as a quiet snare.</summary>\n\tpublic static readonly SnareTone SnaresOff = new(\n\t\tdur: 0.22f, decayFrac: 0.30, hz1: 178f, hz2: 253f, body2: 0.62f, bodyLevel: 0.52f,\n\t\tsag: 0.22f, wire: 0.04f, wireCut: 1350f, wireDrive: 1.0f, level: 1f );\n\n\tpublic static SnareTone For( SnareHit h ) => h switch\n\t{\n\t\tSnareHit.Ghost => Ghost,\n\t\tSnareHit.Rimshot => Rimshot,\n\t\tSnareHit.CrossStick => CrossStick,\n\t\tSnareHit.SnaresOff => SnaresOff,\n\t\t_ => Default,\n\t};\n}\n\n/// <summary>How a three-piece tom set is tuned. The interval is the character; which pitch it\n/// starts from comes from the song's key.</summary>\nenum TomTune\n{\n\t/// <summary>Two stacked perfect fourths \u2014 the conventional tuning, and the one whose fills\n\t/// read as a descending scale rather than as a slide.</summary>\n\tFourths,\n\t/// <summary>Fifths: a wider spread, so each drum is unmistakably its own drum.</summary>\n\tWide,\n\t/// <summary>Stacked major thirds \u2014 close-tuned, so a fill reads as one gesture across a kit\n\t/// rather than as three separate notes.</summary>\n\tThirds,\n\t/// <summary>A fixed physical set that ignores the key entirely. A drummer does not retune\n\t/// between songs, and this is the candidate that says so.</summary>\n\tFixed,\n}\n\n/// <summary>THE KIT, not a pitch: three tom pitches in fixed positions, addressed by INDEX.\n///\n/// 0 is the rack tom (highest), 2 the floor (lowest). Everything downstream takes the index, so\n/// a fill's position across the stereo field comes from which drum was hit \u2014 the old map from a\n/// frequency onto a hardcoded 145\u2013260 Hz range could be, and was, driven off its own bottom end\n/// by the fills that used it. Same trick as Register(octaves): the wrong version is unwriteable.\n/// </summary>\nreadonly struct TomKit\n{\n\tpublic const int Count = 3;\n\n\treadonly float _f0, _f1, _f2;\n\tpublic readonly bool RackLeft;\n\n\tpublic TomKit( float f0, float f1, float f2, bool rackLeft = true )\n\t{\n\t\t_f0 = f0; _f1 = f1; _f2 = f2; RackLeft = rackLeft;\n\t}\n\n\tpublic float Hz( int i ) => i <= 0 ? _f0 : i == 1 ? _f1 : _f2;\n\n\t/// <summary>Where this drum sits, \u22121 hard left \u2026 +1 hard right, before the kit's own spread.\n\t/// The caller scales it (the drums' stereo width is one number and lives outside the kit).\n\t/// </summary>\n\tpublic float Pan( int i )\n\t{\n\t\tfloat u = Math.Clamp( i, 0, Count - 1 ) / (Count - 1f);   // 0 rack \u2026 1 floor\n\t\treturn (RackLeft ? 1f : -1f) * (u * 2f - 1f);\n\t}\n\n\t/// <summary>The tuning for a song in this key. The key sets WHICH pitch the set starts on;\n\t/// the shape sets the intervals. Only the pitch CLASS is read, so the set stays inside a\n\t/// drum-sized range whatever octave the song is written in \u2014 and it is drawn from the song's\n\t/// root rather than a section's key shift, so toms cannot drift mid-song.</summary>\n\tpublic static TomKit Tuned( TomTune shape, int rootMidi, bool rackLeft = true )\n\t{\n\t\tif ( shape == TomTune.Fixed ) return new TomKit( 196f, 147f, 110f, rackLeft );\n\t\tint pc = ((rootMidi % 12) + 12) % 12;\n\t\tint floorMidi = 41 + pc;                       // 87 .. 165 Hz \u2014 a floor tom's range\n\t\tint step = shape switch { TomTune.Wide => 7, TomTune.Thirds => 4, _ => 5 };\n\t\treturn new TomKit( Midi( floorMidi + 2 * step ), Midi( floorMidi + step ),\n\t\t\tMidi( floorMidi ), rackLeft );\n\t}\n}\n\nreadonly struct TomTone\n{\n\tpublic readonly float Dur;\n\tpublic readonly double DecayFrac;\n\tpublic readonly float Sag;            // how far the head's pitch falls over the hit\n\tpublic readonly float SnapMul;        // the inharmonic upper partial, as a ratio\n\tpublic readonly float SnapLevel;\n\tpublic readonly double SnapDecayFrac;\n\tpublic readonly float ClickLevel;     // stick attack\n\tpublic readonly float ClickSec;\n\n\tpublic TomTone( float dur, double decayFrac, float sag, float snapMul, float snapLevel,\n\t\tdouble snapDecayFrac, float clickLevel, float clickSec )\n\t{\n\t\tDur = dur; DecayFrac = decayFrac; Sag = sag; SnapMul = snapMul; SnapLevel = snapLevel;\n\t\tSnapDecayFrac = snapDecayFrac; ClickLevel = clickLevel; ClickSec = clickSec;\n\t}\n\n\tpublic TomTone With( float dur = -1f, double decayFrac = -1.0, float sag = -1f,\n\t\tfloat snapLevel = -1f, float clickLevel = -1f )\n\t\t=> new( dur < 0 ? Dur : dur, decayFrac < 0 ? DecayFrac : decayFrac, sag < 0 ? Sag : sag,\n\t\t\tSnapMul, snapLevel < 0 ? SnapLevel : snapLevel, SnapDecayFrac,\n\t\t\tclickLevel < 0 ? ClickLevel : clickLevel, ClickSec );\n\n\tpublic static readonly TomTone Default = new(\n\t\tdur: 0.18f, decayFrac: 0.3, sag: 0.22f, snapMul: 2.5f, snapLevel: 0.5f,\n\t\tsnapDecayFrac: 0.06, clickLevel: 0.45f, clickSec: 0.006f );\n}\n\n/// <summary>What the hi-hat does. Openness is a continuum and lives outside this \u2014 these are the\n/// articulations that are not simply \"how far open\".</summary>\nenum HatHit { Stick, Foot, Splash }\n\nreadonly struct HatTone\n{\n\tpublic readonly float ClosedDur, OpenDur;\n\tpublic readonly double DecayFrac;\n\tpublic readonly float ClosedCut, OpenCut;\n\tpublic readonly float Level;\n\tpublic readonly float LowThud;      // the foot's pedal-board thump; 0 for a stick hit\n\t/// <summary>An ATTACK RAMP. A stick hit starts instantly; a foot chick does not \u2014 the cymbals\n\t/// travel together and the sound arrives over a few milliseconds. That ramp is the difference\n\t/// between \"shhck\" and a quiet closed hit, and no amount of filtering substitutes for it.</summary>\n\tpublic readonly float AttackSec;\n\t/// <summary>The curve openness travels on. Linear puts half-open half way between a 35 ms tick\n\t/// and an open hat, which is nowhere near half way in what is HEARD \u2014 the ear reads a ratio,\n\t/// not a difference, so the middle of a linear map is still a closed hat.</summary>\n\tpublic readonly float OpenCurve;\n\t/// <summary>Loose cymbals rattling against each other. It peaks at half open, because that is\n\t/// the only place two cymbals are touching AND free to move.</summary>\n\tpublic readonly float SizzleHz, SizzleDepth;\n\n\tpublic HatTone( float closedDur, float openDur, double decayFrac, float closedCut,\n\t\tfloat openCut, float level, float lowThud, float attackSec = 0f, float openCurve = 1f,\n\t\tfloat sizzleHz = 0f, float sizzleDepth = 0f )\n\t{\n\t\tClosedDur = closedDur; OpenDur = openDur; DecayFrac = decayFrac; ClosedCut = closedCut;\n\t\tOpenCut = openCut; Level = level; LowThud = lowThud;\n\t\tAttackSec = attackSec; OpenCurve = openCurve; SizzleHz = sizzleHz; SizzleDepth = sizzleDepth;\n\t}\n\n\tpublic HatTone With( float closedDur = -1f, float openDur = -1f, double decayFrac = -1.0,\n\t\tfloat closedCut = -1f, float openCut = -1f, float level = -1f, float attackSec = -1f,\n\t\tfloat openCurve = -1f, float sizzleHz = -1f, float sizzleDepth = -1f )\n\t\t=> new( closedDur < 0 ? ClosedDur : closedDur, openDur < 0 ? OpenDur : openDur,\n\t\t\tdecayFrac < 0 ? DecayFrac : decayFrac, closedCut < 0 ? ClosedCut : closedCut,\n\t\t\topenCut < 0 ? OpenCut : openCut, level < 0 ? Level : level, LowThud,\n\t\t\tattackSec < 0 ? AttackSec : attackSec, openCurve < 0 ? OpenCurve : openCurve,\n\t\t\tsizzleHz < 0 ? SizzleHz : sizzleHz, sizzleDepth < 0 ? SizzleDepth : sizzleDepth );\n\n\t/// <remarks>openDur here is only the base a song varies FROM: Compose.cs overrides it out of\n\t/// KitNuance.OpenHatDurMin/Max on every song, so this number reaches nothing but the audition\n\t/// path. Change the band, not this.</remarks>\n\tpublic static readonly HatTone Default = new(\n\t\tclosedDur: 0.035f, openDur: 0.16f, decayFrac: 0.4, closedCut: 7000f, openCut: 7000f,\n\t\tlevel: 1f, lowThud: 0f );\n\n\t/// <summary>The foot chick: the pedal closing the cymbals with no stick involved. Duller\n\t/// than a struck closed hat and carrying the board's own thump.</summary>\n\tpublic static readonly HatTone Foot = new(\n\t\tclosedDur: 0.085f, openDur: 0.085f, decayFrac: 0.34, closedCut: 3400f, openCut: 3400f,\n\t\tlevel: 0.9f, lowThud: 0.16f, attackSec: 0.011f );\n\n\t/// <summary>Foot splash: opened and closed again in one motion \u2014 a short open hat with a\n\t/// bright top and no tail to speak of.</summary>\n\tpublic static readonly HatTone Splash = new(\n\t\tclosedDur: 0.26f, openDur: 0.26f, decayFrac: 0.30, closedCut: 8200f, openCut: 8200f,\n\t\tlevel: 0.85f, lowThud: 0.10f );\n\n\tpublic static HatTone For( HatHit h ) => h switch\n\t{\n\t\tHatHit.Foot => Foot,\n\t\tHatHit.Splash => Splash,\n\t\t_ => Default,\n\t};\n}\n\n/// <summary>\n/// THE CYMBAL, DISTILLED \u2014 a measured spectrum spent on thirteen components instead of four\n/// hundred.\n///\n/// Real cymbals were measured for this (provenance below) and the measurement collapsed into three\n/// laws. The first attempt spent them on a MODE FOREST: ~390 resolved partials for the ride, each\n/// with its own ring time. It was accurate, and it was wrong twice over \u2014 it cost ~250 ms of CPU a\n/// hit, and it out-detailed every other voice in the engine by two orders of magnitude. The rest of\n/// this kit is two or three sines and some filtered noise; a cymbal built to a different standard\n/// does not sit in that mix at any level, because the problem is not that it is loud. So the laws\n/// are kept and the spelling is not:\n///\n///   * <b>\u03c4\u00b7\u221af constant</b> \u2192 <b>PER-BAND DECAY</b>. Seven noise bands whose ring times fall as\n///     1/\u221af. This is the whole of what says \"struck metal\" and it is seven numbers.\n///   * <b>a mode forest at constant density</b> \u2192 <b>BAND-LIMITED NOISE</b>. Density the ear cannot\n///     resolve into partials IS noise; four hundred resonators were an expensive way to spell it.\n///   * <b>beating near-pairs</b> \u2192 <b>ONE LOW PAIR</b> of real partials, quiet, down where the ear\n///     resolves the beat and where a cymbal's size is heard.\n///   * <b>strike position as a log-Gaussian bump</b> \u2192 <b>the band gains</b>, one evaluation each.\n///   * splash and wash ride underneath, as they always did.\n///\n/// THIS IS NOT GENERATION 2, AND THE DIFFERENCE IS ONE PROPERTY. Lightly band-passed noise was\n/// tried early and came back \"hats in weird states\" \u2014 correct, and the cause was that it had ONE\n/// decay for the whole voice. A noise band that dies uniformly is a hat. Bands whose ring times\n/// diverge by a factor of five across the spectrum are a cymbal, and that divergence is measured\n/// rather than dialled. Equally it is not generation 5, which made the cymbal out of pure\n/// waveforms and produced church bells every time: the only tonal components here are one quiet\n/// low pair, and everything above them is noise. The two failures bracket the target \u2014 uniform\n/// decay is a hat, resolvable partials are a bell \u2014 and per-band decay is what sits between them.\n///\n/// Provenance (NOT vendored, and must not become a dependency \u2014 what lands is these constants and\n/// this citation): Virtuosity Drums by Versilian Studios &amp; Karoryfer Samples,\n/// github.com/sfzinstruments/virtuosity_drums, CC0-1.0. Measured 2026-08-02 from the overhead-mic\n/// samples \u2014 oh_ride_ride_vl3 (bow), oh_ride_bell_vl3 (bell), oh_crash_crash_vl3 (bright crash),\n/// oh_flatride_crash_vl4 (dark crash). Sustained partials from a 131072-point FFT starting 0.33 s\n/// after onset; per-band ring times from exponential fits over a 4096/1024 STFT. tools/spectool is\n/// the reader and stays in the repo, so every number here can be re-derived \u2014 and --cymbal writes\n/// one dry hit per cymbal to feed it, because a spectrum fitted to a measurement is not fitted\n/// until the RESULT has been measured the same way.\n/// </summary>\nreadonly struct CymbalBands\n{\n\t/// <summary>The noise bands: centre, gain, and the ring time that band decays with.</summary>\n\tpublic readonly float[] Hz, Amp, Tau;\n\tpublic readonly float Dur, Level, Stick, StickCut;\n\tpublic readonly float SplashLvl, SplashTau, WashLvl, WashTau, NoiseHp, WashLp;\n\t/// <summary>Where the SPLASH starts, separately from the wash. A crash's attack is measured\n\t/// broadband and stays that way; a ride's is a stick touching metal, which is a high-frequency\n\t/// event \u2014 and it is the only part of a ride that lands in a band the rest of the arrangement\n\t/// leaves empty. One shared corner meant every attempt to make the stroke cut also added to\n\t/// the mids, where it is masked and does nothing but thicken.</summary>\n\tpublic readonly float SplashHp;\n\n\tCymbalBands( float[] hz, float[] amp, float[] tau, float dur, float level, float stick,\n\t\tfloat stickCut, float splashLvl, float splashTau, float washLvl, float washTau,\n\t\tfloat noiseHp, float washLp, float splashHp )\n\t{\n\t\tHz = hz; Amp = amp; Tau = tau; Dur = dur; Level = level; Stick = stick; StickCut = stickCut;\n\t\tSplashLvl = splashLvl; SplashTau = splashTau; WashLvl = washLvl; WashTau = washTau;\n\t\tNoiseHp = noiseHp; WashLp = washLp; SplashHp = splashHp;\n\t}\n\n\t// \u2500\u2500 The laws \u2500\u2500\n\n\t/// <summary>LAW 1 \u2014 the ring, and it is the law that is NOT shared between cymbals. On the ride\n\t/// every per-band fit lands on \u03c4 \u2248 39/\u221af within take-to-take scatter (230 Hz \u2192 2.6 s, 850 \u2192 1.3,\n\t/// 2.7 k \u2192 0.75, 5.7 k \u2192 0.51), and the long ring is the instrument. Both crashes measured a\n\t/// different shape and each needs one extra term:\n\t///\n\t///  * <paramref name=\"knee\"/> \u2014 a LOW CUT. The bright crash holds \u03c4\u00b7\u221af \u2248 45 only above ~1 kHz;\n\t///    below that its lows die fast (0.9 s at 375 Hz where the bare law says 2.0). A big thin\n\t///    plate struck hard dumps its low modes into the room at once.\n\t///  * <paramref name=\"sizzle\"/> \u2014 a rising FLOOR. The dark crash inverts the ride: low-mids gone\n\t///    in half a second while 7\u201314 kHz rings for 1.5\u20132.0 s. Wash and rivet behaviour rather than\n\t///    plate behaviour, so it is a second term taking over where it is the longer of the two.\n\t/// </summary>\n\tstatic float RingTau( float hz, float k, float knee, float sizzle )\n\t{\n\t\tfloat t = k / MathF.Sqrt( hz );\n\t\tif ( knee > 0f && hz < knee ) t *= hz / knee;\n\t\tif ( sizzle > 0f ) t = MathF.Max( t, sizzle * MathF.Sqrt( hz / SizzleRef ) );\n\t\treturn t;\n\t}\n\tconst float SizzleRef = 8000f;   // where the sizzle term is quoted: dark crash \u03c4 \u2248 1.5 s there\n\n\t/// <summary>LAW 2 \u2014 the band set. The forest's density said the partials are unresolvable, so\n\t/// what matters is only how the energy and the ring vary ACROSS frequency: seven bands,\n\t/// geometrically spaced, is enough resolution for a curve that changes by a factor of five over\n\t/// the whole range. The top must reach ~12 kHz \u2014 the measured sustain holds \u22129\u2026\u221214 dB at\n\t/// 5\u201310 kHz ringing at ~0.5 s, and an earlier version cut off at 4 kHz and measured 10\u201315 dB\n\t/// light against the reference.</summary>\n\t/// <summary>NO TONAL COMPONENTS AT ALL, and the band set reaches down instead. An earlier pass\n\t/// kept one low PAIR of real partials for the measured beating \u2014 two sines a few Hz apart, on\n\t/// the argument that a beat is not a pitch. It is a pitch: 232 Hz ringing for two and a half\n\t/// seconds under noise bands that decay much faster is the most exposed thing in the voice, and\n\t/// it read as a sine sitting inside every cymbal. That is generation 5's church bell arriving\n\t/// through a side door, at a lower level and with a better excuse. The bottom band carries the\n\t/// cymbal's size instead, as noise, which is what the rest of the voice is made of.</summary>\n\tconst int Bands = 8;\n\tconst float BandLo = 180f, BandHi = 12000f;\n\t/// <summary>Band width, as the filter's damping. Wide enough that the filter itself does not\n\t/// ring \u2014 its own decay is under 2 ms at the bottom band \u2014 because the DECAY here is the\n\t/// envelope's job. A resonance that rings is a partial, and partials are what generation 5\n\t/// proved a cymbal must not be made of.</summary>\n\tconst float BandQ = 0.7f;\n\n\t/// <summary>LAW 3 \u2014 a strike position is a spectral bump on a log axis. The bow is one wide\n\t/// bump (the measured sustain is flat 300 Hz\u20133.2 kHz with soft edges); the bell is a narrow\n\t/// clang plus a small low knock where the stick shocks the cup; the bright crash centres at\n\t/// 2.2\u20134.7 kHz over a low knock; the dark crash is low-heavy with a sizzle top that has to be\n\t/// excited before the ring law can let it outlive anything.</summary>\n\tstatic float LogBump( float f, float centre, float width )\n\t{\n\t\tfloat u = MathF.Log( f / centre ) / width;\n\t\treturn MathF.Exp( -0.5f * u * u );\n\t}\n\n\treadonly struct Strike\n\t{\n\t\tpublic readonly float Centre, Width, Centre2, Width2, Level2;\n\t\tpublic Strike( float centre, float width, float centre2 = 0f, float width2 = 0.3f,\n\t\t\tfloat level2 = 0f )\n\t\t{\n\t\t\tCentre = centre; Width = width;\n\t\t\tCentre2 = centre2; Width2 = width2; Level2 = level2;\n\t\t}\n\t\tpublic float Weight( float f )\n\t\t\t=> LogBump( f, Centre, Width ) + (Level2 > 0f ? Level2 * LogBump( f, Centre2, Width2 ) : 0f);\n\t}\n\n\t// THE BOW'S BUMP IS A MIX DECISION AND NOT THE MEASUREMENT. The reference puts a real ride's\n\t// sustain at ~1.2 kHz, and that is where this sat \u2014 which is both darker than the hi-hat it\n\t// stands in for and, worse, exactly where the guitars are. Measured against an open hat as\n\t// spectral centroid: the hat is 12.5 kHz at the attack and STILL 12.4 kHz half a second later,\n\t// because it is one high-passed noise with one decay; the ride was 10.4 kHz falling to 8.8 kHz,\n\t// because tau=k/sqrt(f) means the low bands outlive the high ones and a cymbal built on that law\n\t// ALWAYS darkens as it rings. So a ride reads as the dark voice against a hat that never moves,\n\t// which is backwards for the one carrying the pulse. At 4200 Hz with the noise floor lifted the\n\t// tail comes to 10.9 kHz \u2014 still under the hat, and 2.1 kHz brighter than it was.\n\tstatic readonly Strike BowStrike = new( 4200f, 0.95f );\n\tstatic Strike BellStrike( float clang ) => new( clang, 0.45f, 290f, 0.25f, 0.30f );\n\tstatic readonly Strike BrightCrashStrike = new( 3200f, 0.80f, 400f, 0.55f, 0.25f );\n\tstatic readonly Strike DarkCrashStrike = new( 520f, 0.75f, 9000f, 0.60f, 0.45f );\n\n\t/// <summary>How loud one STROKE is, and it is not the same for a ride and a crash even though\n\t/// the same arm strikes both. A ride stroke rings for seconds and is played eight times a bar,\n\t/// so a riding section has a dozen rings sounding at once; the hi-hat it replaces is 35 ms and\n\t/// never overlaps itself. Level per stroke and level in the mix are two different quantities \u2014\n\t/// measured with --levels, a ride at the crash's stroke level put country's whole kit 2.6 dB\n\t/// over the rest of its band on its riding sections alone. A crash overlaps nothing, being one\n\t/// gesture a phrase, so it keeps the louder stroke.</summary>\n\t// StrokeLevelRide was 0.30 and went to 0.95 in one +10 dB step, by ear, to stop the ride being\n\t// buried \u2014 and that commit's own message flagged the result as \"worth watching\". This is that\n\t// watch firing: measured in the >2.5 kHz band, where nothing else in a ska arrangement lives,\n\t// 0.95 puts the ride +6.9 dB over the ENTIRE REST OF THE MIX and holds that band within 12 dB\n\t// of its own peak 43% of the time. 0.30 was +1.4 dB and 12.8%, which is the buried the boost\n\t// was aimed at; 0.55 was +3.65 dB and 21.3% \u2014 present without being the arrangement. IT IS 0.45\n\t// NOW and those two figures describe the 0.55 it was: the bow's strike bump moved to 4200 Hz\n\t// and the level came down with it, which on the song read +4.74 -> +1.98 dB at 1-3 kHz against\n\t// +7.03 -> +5.54 at 6-14 kHz. Nothing has re-measured the duty cycle at 0.45.\n\t//\n\t// The lesson is about the measurement, not the number: a level set by ear against ONE\n\t// balance (\"can I hear the ride?\") has no way to notice the other one (\"is the ride now the\n\t// loudest thing in its band?\"), and a +10 dB single step is where that goes wrong. The\n\t// duty-cycle-in-band reading answers both and is what the next change to this should use.\n\tconst float StrokeLevelRide = 0.45f, StrokeLevelCrash = 0.60f;\n\n\t/// <summary>The extra decay a cymbal takes on WHILE IT IS BEING PLAYED \u2014 see RenderCymbal's\n\t/// chokeTau. A stroke landing on a ringing cymbal excites it and damps it, because the stick is\n\t/// in contact with the metal, and that is the difference between a ride and a drone. It has to\n\t/// compound over a stroke train the way the physics does: ring time falls with frequency, so at\n\t/// riding eighths a train stacks the 250 Hz band +7.6 dB over a single stroke against +2.4 dB at\n\t/// 5 kHz, and it is the LOW ring that runs away. A flat level cut cannot fix that \u2014 it takes the\n\t/// attack down with the drone. An added decay is frequency-dependent in the right direction: a\n\t/// fixed extra rate costs a 2.5 s band most of its tail and a 0.5 s one very little.</summary>\n\tpublic const float RestrikeTau = 0.70f;\n\n\t/// <summary>How much of the measured crash ring is kept. THIS IS A MIX DECISION AND NOT A\n\t/// MEASUREMENT \u2014 a real crash in a room rings for three or four seconds and the analysis says\n\t/// so, but a crash lands at every phrase end and every section start here, so at that density\n\t/// the full ring never clears before the next one and the arrangement swims. The law stays\n\t/// legible and the departure from it is one number rather than a quietly re-fitted constant.\n\t/// The ride is untouched: it is struck far more often but its strokes damp each other\n\t/// (RestrikeTau), which is the physical version of the same problem.</summary>\n\tconst float CrashRingScale = 0.45f;\n\n\t/// <summary>THE STROKE HAS TO CUT, and level is the wrong lever for that. Measured against the\n\t/// hi-hat it replaces, the ride carries MORE energy \u2014 and it still disappears in a band mix,\n\t/// because of where that energy sits: a hat is high-passed noise with essentially everything\n\t/// above 5 kHz, a band nothing else in the arrangement occupies, while the ride's measured\n\t/// sustain is a bump at 1.2 kHz spreading flat from 180 Hz up, straight through the guitars.\n\t/// Equal energy, very unequal audibility, and raising the level only adds to the part that is\n\t/// masked. The splash is the answer and it is faithful to the measurement: the sustain is\n\t/// mid-centred but the ATTACK is broadband, so a bigger splash is a per-stroke click in the\n\t/// clear rather than more mud. That is what a ride's ping is, and it is inside the 0.5\u20131.8\n\t/// band the audition approved.</summary>\n\t/// <remarks>THE KNEE IS WHY A RIDE STROKE STOPS READING AS A CRASH. Measured per band as the\n\t/// time to fall 20 dB, the ride against the bright crash was low 2.80 s vs 1.00, mid 2.53 vs\n\t/// 0.89, upper-mid 2.03 vs 0.87, top 1.53 vs 0.81 \u2014 while the two spectra's band WEIGHTS are\n\t/// within a dB of each other (mid \u22128.4 dB vs \u22128.5). A ride whose spectral balance is a crash's\n\t/// and whose tail is 2.8\u00d7 longer is a crash that will not stop, and that is what it sounded\n\t/// like wherever the mix got sparse enough to expose it. The crash already carried a knee for\n\t/// this reason; the ride carried none, so its mids rang at the full \u03c4=k/\u221af. At 2 kHz the mid\n\t/// comes to 1.47 s \u2014 still 1.65\u00d7 the crash, because a ride SHOULD sustain longer than one, and\n\t/// no longer the same object. The stick attack is untouched (peak still inside the first 20 ms),\n\t/// which is the point: this shortens the wash behind the ping, not the ping.</remarks>\n\tpublic static CymbalBands Bow( float splash = 1f, float wash = 1f, float ring = 1f )\n\t\t=> Build( BowStrike, tauK: 39f, knee: 3500f, sizzle: 0f, ring: ring,\n\t\t\tsplash: 1.25f * splash, splashTau: 0.10f, washLvl: 0.060f * wash,\n\t\t\twashTau: 0.70f * ring, stick: 0.55f, stickCut: 9000f, noiseHp: 900f, washLp: 6500f,\n\t\t\tlevel: StrokeLevelRide, splashHp: 3200f );\n\n\t/// <summary>The bell. A ride bell is not a church bell: no harmonic stack and no low\n\t/// fundamental \u2014 the measurement puts its energy in a clang cluster around 2.3 kHz over the\n\t/// same metal as the bow.</summary>\n\tpublic static CymbalBands Bell( float splash = 1f, float ring = 1f, float clang = 2300f )\n\t\t=> Build( BellStrike( clang ), tauK: 39f, knee: 3500f, sizzle: 0f, ring: ring,\n\t\t\tsplash: 0.40f * splash, splashTau: 0.05f, washLvl: 0.030f,\n\t\t\twashTau: 0.55f * ring, stick: 0.40f, stickCut: 6500f, noiseHp: 240f, washLp: 6500f,\n\t\t\tlevel: StrokeLevelRide, splashHp: 2600f );\n\n\t/// <summary>The bright crash. THE ROAR IS THE INSTRUMENT: a third of a second in, the\n\t/// measurement resolves essentially no partials at all. So the splash is not an attack\n\t/// transient here, it is a layer with its own third of a second of decay.</summary>\n\tpublic static CymbalBands CrashBright( float splash = 1f, float ring = 1f, float wash = 1f )\n\t\t=> Build( BrightCrashStrike, tauK: 45f, knee: 1000f, sizzle: 0f, ring: ring * CrashRingScale,\n\t\t\tsplash: 2.30f * splash, splashTau: 0.30f, washLvl: 0.55f * wash,\n\t\t\twashTau: 1.05f * ring, stick: 0.10f, stickCut: 6000f, noiseHp: 2400f, washLp: 6200f,\n\t\t\tlevel: StrokeLevelCrash, splashHp: 900f );\n\n\t/// <summary>The dark crash \u2014 a heavier, flatter cymbal crashed rather than ridden, and the\n\t/// opposite shape at both ends: resolved lows that ring, a body gone in half a second, and a\n\t/// top that outlives everything.</summary>\n\tpublic static CymbalBands CrashDark( float splash = 1f, float ring = 1f, float wash = 1f )\n\t\t=> Build( DarkCrashStrike, tauK: 13.5f, knee: 0f, sizzle: 1.5f, ring: ring * CrashRingScale,\n\t\t\tsplash: 1.50f * splash, splashTau: 0.22f, washLvl: 0.35f * wash,\n\t\t\twashTau: 0.90f * ring, stick: 0.10f, stickCut: 4500f, noiseHp: 200f, washLp: 4200f,\n\t\t\tlevel: StrokeLevelCrash, splashHp: 200f );\n\n\tstatic CymbalBands Build( in Strike strike, float tauK, float knee, float sizzle, float ring,\n\t\tfloat splash, float splashTau, float washLvl, float washTau, float stick, float stickCut,\n\t\tfloat noiseHp, float washLp, float level, float splashHp )\n\t{\n\t\tvar hz = new float[Bands]; var am = new float[Bands]; var ta = new float[Bands];\n\t\tfloat step = MathF.Pow( BandHi / BandLo, 1f / (Bands - 1) );\n\t\tfloat e = 0f, maxTau = 0f;\n\t\tfor ( int b = 0; b < Bands; b++ )\n\t\t{\n\t\t\tfloat f = BandLo * MathF.Pow( step, b );\n\t\t\thz[b] = f;\n\t\t\tam[b] = strike.Weight( f );\n\t\t\tta[b] = ring * RingTau( f, tauK, knee, sizzle );\n\t\t\te += am[b] * am[b];\n\t\t\tmaxTau = MathF.Max( maxTau, ta[b] );\n\t\t}\n\t\t// Energy-normalised, so the four strikes land comparably before the stroke level is applied.\n\t\tfloat lvl = level / MathF.Sqrt( MathF.Max( 1e-6f, e ) );\n\t\t// Long enough for the longest component to reach about \u221245 dB, and no longer: every sample\n\t\t// past that is a multiply spent on silence, and this voice is rendered per HIT.\n\t\t// Long enough for the longest band to reach about \u221231 dB and no longer. A cymbal in a room\n\t\t// keeps going past that; a cymbal in a mix with a whole kit over it does not, and every\n\t\t// sample past the point it stops being audible is a multiply spent on silence.\n\t\tfloat dur = Math.Clamp( maxTau * 3.6f, 0.6f, 3.0f );\n\t\treturn new CymbalBands( hz, am, ta, dur, lvl, stick, stickCut,\n\t\t\tsplash, splashTau, washLvl, washTau, noiseHp, washLp, splashHp );\n\t}\n}\n\n/// <summary>This song's cymbals as NUMBERS \u2014 the per-song point each of KitNuance's cymbal bands\n/// sits at. Drawn in ComposePlan so every genre pulls the same values in the same order, for the\n/// same reason a kick's click corner is drawn there: a kit is a physical object and the same\n/// cymbal does not make the identical sound twice.</summary>\nreadonly struct CymbalDraw\n{\n\tpublic readonly float RideSplash, RideWash, RideRing, BellClang, BellRing;\n\tpublic readonly float BrightSplash, BrightRing, DarkSplash, DarkWash, DarkRing;\n\n\tCymbalDraw( float rideSplash, float rideWash, float rideRing, float bellClang, float bellRing,\n\t\tfloat brightSplash, float brightRing, float darkSplash, float darkWash, float darkRing )\n\t{\n\t\tRideSplash = rideSplash; RideWash = rideWash; RideRing = rideRing;\n\t\tBellClang = bellClang; BellRing = bellRing;\n\t\tBrightSplash = brightSplash; BrightRing = brightRing;\n\t\tDarkSplash = darkSplash; DarkWash = darkWash; DarkRing = darkRing;\n\t}\n\n\tpublic static readonly CymbalDraw Default = new( 1f, 1f, 1f, 2300f, 1f, 1f, 1f, 1f, 1f, 1f );\n\n\tpublic static CymbalDraw Draw( Rng rng ) => new(\n\t\tKitNuance.At( KitNuance.RideSplashMin, KitNuance.RideSplashMax, rng.Next() ),\n\t\tKitNuance.At( KitNuance.RideWashMin, KitNuance.RideWashMax, rng.Next() ),\n\t\tKitNuance.At( KitNuance.RideRingMin, KitNuance.RideRingMax, rng.Next() ),\n\t\tKitNuance.At( KitNuance.BellClangMin, KitNuance.BellClangMax, rng.Next() ),\n\t\tKitNuance.At( KitNuance.BellRingMin, KitNuance.BellRingMax, rng.Next() ),\n\t\tKitNuance.At( KitNuance.CrashSplashMin, KitNuance.CrashSplashMax, rng.Next() ),\n\t\tKitNuance.At( KitNuance.CrashRingMin, KitNuance.CrashRingMax, rng.Next() ),\n\t\tKitNuance.At( KitNuance.DarkSplashMin, KitNuance.DarkSplashMax, rng.Next() ),\n\t\tKitNuance.At( KitNuance.DarkWashMin, KitNuance.DarkWashMax, rng.Next() ),\n\t\tKitNuance.At( KitNuance.DarkRingMin, KitNuance.DarkRingMax, rng.Next() ) );\n}\n\npublic sealed partial class MusicGen\n{\n\t/// <summary>A deterministic per-hit stick/round-robin stream. A LOCAL LFSR seeded on the hit's\n\t/// own sample position: two hits differ, the same hit is always the same, and the shared drum\n\t/// RNG stream \u2014 and therefore every other pattern in the song \u2014 is left byte-identical.</summary>\n\tstatic uint HitSeed( int start ) => (uint)start * 2654435761u | 1u;\n\n\tstatic float HitNext( ref uint s )\n\t{\n\t\ts ^= s << 13; s ^= s >> 17; s ^= s << 5;\n\t\treturn (s & 0xffff) / 32768f - 1f;      // \u22121 .. 1\n\t}\n\n\t// \u2500\u2500 Kick \u2500\u2500\n\t// The kit's three struck voices take a LEVEL, defaulting to 1 so the groove path reads\n\t// exactly as it did. It exists for the fill, which is a phrase and needs dynamics: an even\n\t// stream of equally-loud hits reads as a wall however few of them there are.\n\tvoid RenderKick( int start, Rng noise, float amp = 1f )\n\t\t=> RenderKick( start, noise, amp, KickTone.Default, 0f );\n\n\t/// <param name=\"pan\">\u22121 \u2026 +1. A double pedal is two beaters on two sides of one drum, so the\n\t/// alternation is a POSITION, not a second kick sound. 0 is the single pedal.</param>\n\tinternal void RenderKick( int start, Rng noise, float amp, in KickTone k, float pan )\n\t{\n\t\tif ( _drumGain <= 0f ) return;\n\t\tstart = Math.Max( 0, start + _time.DrumPush );\n\n\t\t// Round-robin: no two strokes of a real pedal are the same, and a straight sixteenth run\n\t\t// is where identical ones stop reading as a drum. Pitch and level move together, the way\n\t\t// a harder stroke does.\n\t\tfloat jp = 1f, jl = 1f;\n\t\tif ( k.Jitter > 0f )\n\t\t{\n\t\t\tuint js = HitSeed( start );\n\t\t\tjp = 1f + k.Jitter * 0.09f * HitNext( ref js );\n\t\t\tjl = 1f + k.Jitter * 0.16f * HitNext( ref js );\n\t\t}\n\n\t\tfloat gL = 1f, gR = 1f;\n\t\tif ( pan != 0f ) StereoGains( pan, out gL, out gR );\n\n\t\tint dur = (int)(_sr * k.Dur);\n\t\tdouble decay = dur * k.DecayFrac;\n\t\tdouble subDecay = dur * k.SubDecayFrac;\n\t\tdouble phase = 0, subPhase = 0;\n\t\t// noise.Next() only fires inside the click below, so changing dur/decay here does NOT\n\t\t// shift the drum RNG stream (patterns are preserved).\n\t\tint clickLen = (int)(_sr * k.ClickSec);\n\t\tfloat ca = k.ClickCut > 0f ? LpCoeff( k.ClickCut ) : 0f;\n\t\tfloat clickLp = 0f;\n\t\tint end = Math.Min( _bufL.Length, start + dur );\n\t\tfor ( int i = 0; start + i < end; i++ )\n\t\t{\n\t\t\tfloat t = (float)i / dur;\n\t\t\tphase += (k.StartHz - k.DropHz * MathF.Min( 1f, t * k.DropRate )) * jp / _sr;\n\t\t\tsubPhase += k.SubHz / _sr;\n\t\t\tfloat env = (float)Math.Exp( -i / decay );\n\t\t\tfloat subEnv = (float)Math.Exp( -i / subDecay );\n\t\t\tfloat body = (float)Math.Tanh( MathF.Sin( (float)(phase * 2 * Math.PI) ) * k.Drive ) * env;\n\t\t\tfloat sub = MathF.Sin( (float)(subPhase * 2 * Math.PI) ) * k.SubLevel * subEnv;\n\t\t\tfloat click = 0f;\n\t\t\tif ( i < clickLen )\n\t\t\t{\n\t\t\t\tfloat cn = noise.Next() * 2f - 1f;\n\t\t\t\tif ( k.ClickCut > 0f ) { clickLp += ca * (cn - clickLp); cn = clickLp; }\n\t\t\t\tclick = cn * k.ClickLevel * (1f - i / (float)clickLen);\n\t\t\t}\n\t\t\tfloat v = (body + sub + click) * amp * jl * _c.KickVol * _c.KickBalance * _drumGain * _drumLowMul;\n\t\t\t_bufL[start + i] += v * gL; _bufR[start + i] += v * gR;\n\t\t}\n\n\t\t// The beater coming back off the head. It is the SAME stroke, not a second note, so it\n\t\t// carries no click of its own and cannot recurse.\n\t\tif ( k.Beater > 0f )\n\t\t\tRenderKick( start + (int)(_sr * k.BeaterSec), noise, amp * k.Beater,\n\t\t\t\tk.With( clickLevel: 0f, beater: 0f, jitter: 0f ), pan );\n\t}\n\n\t// One-pole high-pass coefficient (unconditionally stable).\n\tfloat HpCoeff( float fc ) => (float)(1.0 / (1.0 + 2 * Math.PI * fc / _sr));\n\n\t// One-pole low-pass coefficient.\n\tfloat LpCoeff( float fc ) => (float)(1.0 - Math.Exp( -2 * Math.PI * fc / _sr ));\n\n\t// \u2500\u2500 Snare \u2500\u2500\n\tvoid RenderSnare( int start, Rng noise, bool ghost, float level = 1f )\n\t\t=> RenderSnare( start, noise, level, ghost ? SnareTone.Ghost : SnareTone.Default );\n\n\tinternal void RenderSnare( int start, Rng noise, float level, in SnareTone s )\n\t{\n\t\tif ( _drumGain <= 0f ) return;\n\t\tstart = Math.Max( 0, start + _time.DrumPush );\n\t\t// dur and the single noise.Next()/sample are kept exactly so the drum RNG stream\n\t\t// is unchanged \u2014 only the timbre is a parameter.\n\t\tint dur = (int)(_sr * s.Dur);\n\t\tdouble decay = dur * s.DecayFrac;\n\t\tdouble phase = 0, phase2 = 0;\n\t\tfloat amp2 = level * _c.SnareVol * _c.SnareBalance * s.Level * _drumGain;\n\t\tfloat a = HpCoeff( s.WireCut );\n\t\tvar crack = s.CrackLevel > 0f ? new BandPass( s.CrackHz, s.CrackQ, _sr ) : default;\n\t\tdouble crackDecay = dur * s.CrackDecayFrac;\n\t\tdouble thudPhase = 0;\n\t\tfloat inPrev = 0f, outPrev = 0f;\n\t\tint end = Math.Min( _bufL.Length, start + dur );\n\t\tfor ( int i = 0; start + i < end; i++ )\n\t\t{\n\t\t\tfloat t = (float)i / dur;\n\t\t\tfloat env = (float)Math.Exp( -i / decay );\n\t\t\tfloat drop = 1f - s.Sag * t;       // shell pitch sags a touch \u2192 \"dow\"\n\t\t\tphase += s.Hz1 * drop / _sr;\n\t\t\tphase2 += s.Hz2 * drop / _sr;\n\t\t\tfloat n = noise.Next() * 2f - 1f;\n\t\t\tfloat hp = a * (outPrev + n - inPrev); inPrev = n; outPrev = hp;\n\t\t\tfloat body = (MathF.Sin( (float)(phase * 2 * Math.PI) )\n\t\t\t\t+ MathF.Sin( (float)(phase2 * 2 * Math.PI) ) * s.Body2) * s.BodyLevel;\n\t\t\tfloat v = ((float)Math.Tanh( hp * s.WireDrive ) * s.Wire + body) * env;\n\t\t\tif ( s.CrackLevel > 0f )\n\t\t\t\tv += crack.Next( n ) * s.CrackLevel * (float)Math.Exp( -i / crackDecay );\n\t\t\tif ( s.ThudLevel > 0f )\n\t\t\t{\n\t\t\t\tthudPhase += s.ThudHz / _sr;\n\t\t\t\tv += MathF.Sin( (float)(thudPhase * 2 * Math.PI) ) * s.ThudLevel\n\t\t\t\t\t* (float)Math.Exp( -i / (dur * 0.09) );\n\t\t\t}\n\t\t\tv = v * amp2;\n\t\t\t_bufL[start + i] += v; _bufR[start + i] += v;\n\t\t}\n\t}\n\n\tinternal void RenderSnare( int start, Rng noise, SnareHit hit, float amp = 1f )\n\t\t=> RenderSnare( start, noise, amp, SnareTone.For( hit ) );\n\n\t/// <summary>A flam: the grace note is a hand that arrives early and quieter, and the pair is\n\t/// heard as ONE thickened stroke rather than as two notes. The spacing is in MILLISECONDS \u2014\n\t/// it is a physical property of two sticks, not a subdivision, so it must not scale with\n\t/// tempo (see the strum-spread note in CLAUDE.md).</summary>\n\tinternal void RenderSnareFlam( int start, Rng noise, float amp = 1f, float graceMs = 24f,\n\t\tfloat graceLevel = 0.55f )\n\t{\n\t\tRenderSnare( start - (int)(_sr * graceMs * 0.001f), noise, amp * graceLevel, SnareTone.Default );\n\t\tRenderSnare( start, noise, amp, SnareTone.Default );\n\t}\n\n\t/// <summary>A buzz/press roll across a span: the stick is leaned into the head and the\n\t/// bounces run together. Many quiet, closely-spaced strokes, so it is a texture with a\n\t/// crescendo rather than a rhythm.</summary>\n\tinternal void RenderSnareBuzz( int start, int spanSamples, Rng noise, float fromAmp = 0.18f,\n\t\tfloat toAmp = 0.85f, float spacingMs = 26f )\n\t{\n\t\tint step = Math.Max( 1, (int)(_sr * spacingMs * 0.001f) );\n\t\tvar tone = SnareTone.Default.With( dur: 0.05f, decayFrac: 0.26, wire: 0.85f );\n\t\tfor ( int i = 0, at = 0; at < spanSamples; i++, at += step )\n\t\t{\n\t\t\tfloat u = spanSamples <= step ? 1f : at / (float)spanSamples;\n\t\t\tuint s = HitSeed( start + at );\n\t\t\tfloat wobble = 1f + 0.18f * HitNext( ref s );\n\t\t\tRenderSnare( start + at, noise, (fromAmp + (toAmp - fromAmp) * u) * wobble, tone );\n\t\t}\n\t}\n\n\t// \u2500\u2500 Toms \u2500\u2500\n\n\t/// <summary>A tom of the kit, by INDEX. 0 is the rack, 2 the floor; where it sits in the\n\t/// field is a property of which drum it is, so no fill can drive it off the end of a range.\n\t/// </summary>\n\tinternal void RenderTom( int start, in TomKit kit, int index, Rng noise, float amp, in TomTone tone )\n\t{\n\t\tif ( _drumGain <= 0f ) return;\n\t\tStereoGains( -_drumPan * kit.Pan( index ), out float gL, out float gR );\n\t\tRenderTomAt( start, kit.Hz( index ), gL, gR, amp, tone );\n\t}\n\n\tvoid RenderTomAt( int start, float baseFreq, float gL, float gR, float amp, in TomTone k )\n\t{\n\t\tstart = Math.Max( 0, start + _time.DrumPush );\n\t\tint dur = (int)(_sr * k.Dur);\n\t\tdouble decay = dur * k.DecayFrac;\n\t\tdouble attackDecay = dur * k.SnapDecayFrac;   // fast-decaying upper partial \u2192 beater \"snap\"\n\t\tdouble phase = 0, phase2 = 0;\n\t\tuint ns = HitSeed( start );\n\t\tint clickLen = (int)(_sr * k.ClickSec);\n\t\tint end = Math.Min( _bufL.Length, start + dur );\n\t\tfor ( int i = 0; start + i < end; i++ )\n\t\t{\n\t\t\tfloat t = (float)i / dur;\n\t\t\tfloat pf = baseFreq * (1f - k.Sag * t);    // pitch sag: how far the head lets go\n\t\t\tphase += pf / _sr;\n\t\t\tphase2 += pf * k.SnapMul / _sr;            // inharmonic upper partial for attack snap\n\t\t\tfloat env = (float)Math.Exp( -i / decay );\n\t\t\tfloat aenv = (float)Math.Exp( -i / attackDecay );\n\t\t\tfloat body = MathF.Sin( (float)(phase * 2 * Math.PI) ) * env;\n\t\t\tfloat snap = MathF.Sin( (float)(phase2 * 2 * Math.PI) ) * aenv * k.SnapLevel;\n\t\t\tfloat click = 0f;\n\t\t\tif ( i < clickLen )\n\t\t\t\tclick = HitNext( ref ns ) * k.ClickLevel * (1f - i / (float)clickLen);\n\t\t\tfloat v = (body + snap + click) * amp * _c.TomVol * _c.TomBalance * _drumGain * _drumLowMul;\n\t\t\t_bufL[start + i] += v * gL; _bufR[start + i] += v * gR;\n\t\t}\n\t}\n\n\t// \u2500\u2500 Hats \u2500\u2500\n\n\t/// <summary>The hi-hat. OPENNESS IS A CONTINUUM \u2014 a pedal is a distance, not a switch \u2014 and\n\t/// <paramref name=\"chokeAt\"/> is where the foot closes it again. An open hat with nothing\n\t/// choking it rings through whatever comes next, which is why pop's open-on-every-offbeat\n\t/// smears: the tail is longer than the gap. The open\u2192closed pair is the gesture.</summary>\n\t/// <param name=\"chokeAt\">Absolute sample position the foot closes at, or int.MaxValue.</param>\n\tinternal void RenderHat( int start, float openness, float amp, Rng noise, in HatTone h,\n\t\tint chokeAt = int.MaxValue )\n\t{\n\t\tif ( _drumGain <= 0f ) return;\n\t\tstart = Math.Max( 0, start + _time.DrumPush );\n\t\t// Closed/open hats live on the left of the kit (the hi-hat stand); ride sits opposite.\n\t\tStereoGains( -_drumPan, out float gL, out float gR );\n\t\topenness = Math.Clamp( openness, 0f, 1f );\n\t\t// THE MAP IS GEOMETRIC. A closed hat and an open one are a factor of seventeen apart in\n\t\t// length, and the ear reads that as a RATIO rather than as a difference: interpolated\n\t\t// linearly, most of the pedal's travel lands within a few percent of fully open, and two\n\t\t// positions a third of the range apart are the same hat. Stepping by a constant ratio puts\n\t\t// an even amount of audible change under every part of the travel, which is what a foot\n\t\t// lifting steadily has to sound like.\n\t\tfloat u = h.OpenCurve == 1f || openness <= 0f || openness >= 1f\n\t\t\t? openness : MathF.Pow( openness, h.OpenCurve );\n\t\tfloat durSec = openness <= 0f ? h.ClosedDur\n\t\t\t: openness >= 1f ? h.OpenDur\n\t\t\t: h.ClosedDur * MathF.Pow( h.OpenDur / h.ClosedDur, u );\n\t\tfloat cut = openness <= 0f ? h.ClosedCut\n\t\t\t: openness >= 1f ? h.OpenCut\n\t\t\t: h.ClosedCut * MathF.Pow( h.OpenCut / h.ClosedCut, u );\n\t\tint dur = (int)(_sr * durSec);\n\t\tdouble decay = dur * h.DecayFrac;\n\t\tfloat a = HpCoeff( cut );\n\t\t// The choke itself: the cymbals meeting is a fast release, not a cut \u2014 a hard stop\n\t\t// clicks, and a drummer's foot is not instantaneous either.\n\t\tfloat chokeStep = (float)Math.Exp( -1.0 / Math.Max( 1.0, _sr * 0.012 ) );\n\t\tfloat chokeEnv = 1f;\n\t\tdouble lowPhase = 0, sizzlePhase = 0;\n\t\tint attack = (int)(_sr * h.AttackSec);\n\t\t// Loose cymbals rattle most when they are half touching, and not at all when open or shut.\n\t\tfloat sizzle = h.SizzleDepth * 4f * u * (1f - u);\n\t\tfloat inPrev = 0f, outPrev = 0f;\n\t\tint end = Math.Min( _bufL.Length, start + dur );\n\t\tfor ( int i = 0; start + i < end; i++ )\n\t\t{\n\t\t\tfloat env = (float)Math.Exp( -i / decay );\n\t\t\tif ( attack > 0 && i < attack ) env *= i / (float)attack;\n\t\t\tif ( sizzle > 0f )\n\t\t\t{\n\t\t\t\tsizzlePhase += h.SizzleHz / _sr;\n\t\t\t\tenv *= 1f - sizzle * 0.5f * (1f + MathF.Sin( (float)(sizzlePhase * 2 * Math.PI) ));\n\t\t\t}\n\t\t\tfloat n = noise.Next() * 2f - 1f;\n\t\t\tfloat hp = a * (outPrev + n - inPrev); inPrev = n; outPrev = hp;\n\t\t\tfloat v = hp * env;\n\t\t\tif ( h.LowThud > 0f )\n\t\t\t{\n\t\t\t\tlowPhase += 96f / _sr;\n\t\t\t\tv += MathF.Sin( (float)(lowPhase * 2 * Math.PI) ) * h.LowThud\n\t\t\t\t\t* (float)Math.Exp( -i / (dur * 0.12) );\n\t\t\t}\n\t\t\tif ( start + i >= chokeAt ) chokeEnv *= chokeStep;\n\t\t\t// Left to right, and the two new factors last: at their neutral 1f they are exact\n\t\t\t// identities, so the groove path's arithmetic is unchanged to the bit.\n\t\t\tv = v * amp * _c.HatBalance * _drumGain * _drumHighMul * chokeEnv * h.Level;\n\t\t\t_bufL[start + i] += v * gL; _bufR[start + i] += v * gR;\n\t\t}\n\t}\n\n\tinternal void RenderHat( int start, HatHit hit, float amp, Rng noise )\n\t\t=> RenderHat( start, hit == HatHit.Splash ? 1f : 0f, amp, noise, HatTone.For( hit ) );\n\n\t// \u2500\u2500 Cymbals \u2500\u2500\n\t// The ride, its bell, and the two crashes: one voice, four sets of constants (CymbalBands).\n\t// Rendered PER HIT, like every other voice in this kit \u2014 seven filtered-noise bands and two\n\t// partials is cheap enough that a riding section can afford it, which is the whole reason the\n\t// distilled version exists.\n\n\t/// <summary>A hand on the metal: the cymbal stops. Fast, but not a cut \u2014 a hard stop clicks,\n\t/// and a hand is not instantaneous either.</summary>\n\tinternal const float HandChoke = 0.020f;\n\n\t/// <param name=\"chokeAt\">Absolute sample position something lands on the cymbal, or\n\t/// int.MaxValue.</param>\n\t/// <param name=\"chokeTau\">How fast it takes the ring away. <see cref=\"HandChoke\"/> is a hand\n\t/// and the cymbal is gone; <see cref=\"CymbalBands.RestrikeTau\"/> is the STICK LANDING AGAIN,\n\t/// which is the same event seen from the other end and is not a smaller choke \u2014 it is a\n\t/// shorter decay for as long as the cymbal is being played, so it compounds over a stroke\n\t/// train the way the physics does.</param>\n\t/// <summary>The ride and its bell, on the right of the kit opposite the hats.</summary>\n\tvoid RenderRideCym( int at, float amp, float[][] t, int chokeAt = int.MaxValue,\n\t\tfloat chokeTau = HandChoke )\n\t\t=> RenderCymbal( at, amp, t, _c.RideBalance, _drumPan, chokeAt, chokeTau );\n\n\t/// <summary>A crash. The kit's two crashes are panned apart and which side each is on is the\n\t/// song's own draw, so a ridden crash and the one accenting over it land opposite each other\n\t/// for free.</summary>\n\tvoid RenderCrashCym( int at, float amp, float[][] t, bool dark,\n\t\tint chokeAt = int.MaxValue, float chokeTau = HandChoke )\n\t\t=> RenderCymbal( at, amp, t, _c.CrashBalance,\n\t\t\tdark == _crashBrightLeft ? _drumPan : -_drumPan, chokeAt, chokeTau );\n\n\t/// <summary>\n\t/// SYNTHESISED ONCE PER SONG, THEN STAMPED. The distillation fixed what the cymbal IS; it does\n\t/// not fix what a ride COSTS, because that is a property of the pattern: a 2.5-second ring\n\t/// struck eight times a bar overlaps itself twenty deep, and rendering each stroke in full pays\n\t/// for all of it. Synthesising the object once and adding it per hit is what a sampler does,\n\t/// and it is honest here for the same reason the bands are \u2014 a cymbal is one physical object\n\t/// and every strike is that object.\n\t///\n\t/// It is two tables, split at 2.5 kHz, because a soft stroke is DARKER and not merely quieter.\n\t/// Variants are round robins: they cost about three milliseconds each now, where the mode\n\t/// forest's cost a quarter of a second, so the repeat-tell is cheap to break.\n\t/// </summary>\n\tinternal float[][] BuildCymbal( in CymbalBands c, int variant )\n\t{\n\t\tint dur = (int)(_sr * c.Dur);\n\t\tvar lo = new float[dur]; var hi = new float[dur];\n\t\tSynthCymbal( c, lo, hi, (uint)(variant * 2654435761u) | 1u );\n\t\treturn new[] { lo, hi };\n\t}\n\n\t/// <param name=\"chokeAt\">Absolute sample position something lands on the cymbal.</param>\n\t/// <param name=\"chokeTau\">How fast it takes the ring away \u2014 <see cref=\"HandChoke\"/> for a hand,\n\t/// <see cref=\"CymbalBands.RestrikeTau\"/> for the stick landing again.</param>\n\tinternal void RenderCymbal( int start, float amp, float[][] t, float balance, float pan,\n\t\tint chokeAt = int.MaxValue, float chokeTau = HandChoke )\n\t{\n\t\tif ( _drumGain <= 0f || amp <= 0f || t == null ) return;\n\t\tstart = Math.Max( 0, start + _time.DrumPush );\n\t\tint end = Math.Min( _bufL.Length, start + t[0].Length );\n\t\tif ( end <= start ) return;\n\t\tStereoGains( pan, out float gL, out float gR );\n\t\tuint js = HitSeed( start );\n\t\tfloat jit = 1f + 0.07f * HitNext( ref js );\n\t\tfloat bus = balance * _drumGain * _drumHighMul * jit;\n\t\tfloat loG = amp * bus;\n\t\t// A soft stroke is darker, not merely quieter \u2014 a stick that does not dig in leaves the top\n\t\t// of the cymbal alone.\n\t\tfloat hiG = amp * MathF.Pow( Math.Clamp( amp, 0.05f, 1f ), 0.35f ) * bus;\n\t\tfloat chokeStep = (float)Math.Exp( -1.0 / Math.Max( 1.0, _sr * (double)chokeTau ) );\n\t\tfloat chokeEnv = 1f;\n\t\tfor ( int i = 0; start + i < end; i++ )\n\t\t{\n\t\t\tif ( start + i >= chokeAt )\n\t\t\t{\n\t\t\t\tchokeEnv *= chokeStep;\n\t\t\t\tif ( chokeEnv < 1e-4f ) break;\n\t\t\t}\n\t\t\tfloat v = (t[0][i] * loG + t[1][i] * hiG) * chokeEnv;\n\t\t\t_bufL[start + i] += v * gL; _bufR[start + i] += v * gR;\n\t\t}\n\t}\n\n\t/// <summary>The voice itself: seven filtered-noise bands with their own decays, the low pair,\n\t/// splash and wash. Written into a lo/hi pair so a stroke's brightness can vary at stamp time.\n\t/// </summary>\n\tvoid SynthCymbal( in CymbalBands c, float[] outLo, float[] outHi, uint seed )\n\t{\n\t\tint dur = outLo.Length;\n\n\t\tint nb = c.Hz.Length;\n\t\tvar bp = new BandPass[nb];\n\t\tvar env = new float[nb]; var dec = new float[nb];\n\t\tvar dies = new int[nb];\n\t\tfor ( int b = 0; b < nb; b++ )\n\t\t{\n\t\t\tbp[b] = new BandPass( c.Hz[b], CymbalBandQ, _sr );\n\t\t\tenv[b] = 1f;\n\t\t\tdec[b] = (float)Math.Exp( -1.0 / (_sr * (double)c.Tau[b]) );\n\t\t\t// Each band stops when it stops being worth its multiplies; they differ by a factor of\n\t\t\t// five in ring time, so most are gone long before the table is.\n\t\t\tdies[b] = Math.Min( dur, (int)(_sr * c.Tau[b] * 5.0f) + 1 );\n\t\t}\n\n\t\tuint ns = seed;\n\n\t\tint stickLen = (int)(_sr * 0.004f);\n\t\tfloat sa = c.StickCut > 0f ? LpCoeff( c.StickCut ) : 0f;\n\t\tfloat lp = 0f;\n\t\tfloat hpA = HpCoeff( c.NoiseHp ), washLpA = LpCoeff( c.WashLp );\n\t\tfloat splHpA = HpCoeff( c.SplashHp );\n\t\tfloat nInPrev = 0f, nHpPrev = 0f, washLp = 0f, sInPrev = 0f, sHpPrev = 0f;\n\t\tdouble splDecay = _sr * (double)Math.Max( 0.005f, c.SplashTau );\n\t\tdouble washDecay = _sr * (double)Math.Max( 0.02f, c.WashTau );\n\t\t// The fade keeps the truncation silent: the longest band still has tail left at the end.\n\t\tint fade = (int)(_sr * 0.10f);\n\t\tfor ( int i = 0; i < dur; i++ )\n\t\t{\n\t\t\tfloat n = noiseNext( ref ns );\n\t\t\tfloat lo = 0f, hi = 0f;\n\t\t\tfor ( int b = 0; b < nb; b++ )\n\t\t\t{\n\t\t\t\tif ( i >= dies[b] ) continue;\n\t\t\t\tfloat v = bp[b].Next( n ) * c.Amp[b] * env[b];\n\t\t\t\tenv[b] *= dec[b];\n\t\t\t\tif ( c.Hz[b] >= BandSplit ) hi += v; else lo += v;\n\t\t\t}\n\t\t\t// The splash (broadband, and on a crash it keeps going for a third of a second) and the\n\t\t\t// wash (the air, darker and long) \u2014 the layers the mode forest never carried anyway.\n\t\t\tfloat hp = hpA * (nHpPrev + n - nInPrev); nInPrev = n; nHpPrev = hp;\n\t\t\twashLp += washLpA * (hp - washLp);\n\t\t\t// The splash rides in the high table and the wash in the low, so each tilts with the\n\t\t\t// layer it belongs to.\n\t\t\tfloat shp = splHpA * (sHpPrev + n - sInPrev); sInPrev = n; sHpPrev = shp;\n\t\t\thi += shp * c.SplashLvl * (float)Math.Exp( -i / splDecay );\n\t\t\tlo += washLp * c.WashLvl * (float)Math.Exp( -i / washDecay );\n\t\t\tif ( c.Stick > 0f && i < stickLen )\n\t\t\t{\n\t\t\t\tfloat sn = HitNext( ref ns );\n\t\t\t\tif ( c.StickCut > 0f ) { lp += sa * (sn - lp); sn = lp; }\n\t\t\t\thi += sn * c.Stick * (1f - i / (float)stickLen);\n\t\t\t}\n\t\t\tint rem = dur - i;\n\t\t\tfloat k = c.Level * (rem < fade ? rem / (float)fade : 1f);\n\t\t\toutLo[i] = lo * k; outHi[i] = hi * k;\n\t\t}\n\t}\n\n\t/// <summary>Where the tilt splits the cymbal: above this is stick and shimmer, below it is the\n\t/// body.</summary>\n\tconst float BandSplit = 2500f;\n\n\tconst float CymbalBandQ = 0.7f;\n\n\t/// <summary>The cymbal's noise source. Its own per-hit stream, so no two strokes are the same\n\t/// waveform and the shared drum RNG is untouched \u2014 the thing a rendered-once table had to buy\n\t/// back with round robins, and gets here for free.</summary>\n\tstatic float noiseNext( ref uint s ) => HitNext( ref s );\n}\n"
        },
        {
            "Ident": "gamah.skafinity",
            "Path": "Code/Engine/Melody.cs",
            "FileName": "Melody.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 341414,
            "IsPrivate": false,
            "Code": "using System;\nusing System.Collections.Generic;\n\nnamespace Skafinity;\n\n/// <summary>\n/// The TUNE \u2014 the part of a song a listener could hum back.\n///\n/// Everything the engine generated before this was accompaniment plus an improvisation: the\n/// chordal voices played a rhythm figure, and the lead invented a fresh phrase every two bars.\n/// That is a backing track, not a song. Real rock, punk, ska and pop songs are built on a\n/// MELODY that recurs \u2014 the chorus states the same tune every time it comes round, and that\n/// repetition is what makes it a chorus rather than another eight bars.\n///\n/// A tune is a <see cref=\"Pattern\"/> whose cell values are SCALE DEGREES relative to the key's\n/// tonic (not to the current chord), so the line keeps its shape while the harmony moves under\n/// it \u2014 which is what a melody is. <see cref=\"MusicGen.RenderTune\"/> resolves a degree against\n/// the bar's chord on the strong beats, so the tune stays consonant without being re-written\n/// chord by chord.\n///\n/// Because it is a Pattern it inherits everything patterns get: it anchors to the section (so a\n/// four-bar tune restarts with the chorus) and it stretches under a half-time feel.\n/// </summary>\nstatic class Melody\n{\n\t/// <summary>Cell value for a rest \u2014 no onset, the previous note holds.</summary>\n\tpublic const int Rest = Harmony.Rest;\n\n\t/// <summary>The AMBITUS \u2014 the range a whole tune is written in, in SCALE DEGREES from the key's\n\t/// tonic: from the sixth below it up to the third above the octave. Twelve degrees, about an\n\t/// octave and a fifth in a major scale, and deliberately lopsided \u2014 a melody sits above its\n\t/// tonic and only dips under it, so a symmetric range would spend half of itself where no tune\n\t/// goes.\n\t///\n\t/// It is an authored bound rather than a measured one: what it is FOR is that a line which\n\t/// wanders further stops being singable, and singable is what makes the thing a tune. The\n\t/// number is a judgement about that and nothing more.</summary>\n\tpublic const int DegreeMin = -2, DegreeMax = 9;\n\n\t/// <summary>How many scale degrees ONE PHRASE may cover \u2014 eight, an octave in a major scale,\n\t/// inside the twelve the whole tune may reach.\n\t///\n\t/// A RANGE AND AN AMBITUS ARE TWO DIFFERENT NUMBERS AND ONE CANNOT DO BOTH JOBS. The ambitus is\n\t/// a whole-song figure \u2014 how far the tune goes over all of it \u2014 and a tune here is 2\u20138 bars, so\n\t/// bounding a single phrase with it was measuring one thing and spending it on another. What a\n\t/// phrase actually does is orbit a register: it opens somewhere, moves about an octave around\n\t/// that, and the tune gets its wider reach from the phrases sitting in DIFFERENT places rather\n\t/// than from any one of them wandering.\n\t///\n\t/// So the window is drawn per phrase and anchored on the note the phrase opens on\n\t/// (<see cref=\"Opens\"/>), which is why the opening degree keeps its weighting instead of being\n\t/// folded into a window drawn first. Inside a phrase this is the bound the walk reflects off\n\t/// and the centre <see cref=\"Centre\"/> pulls toward; the ambitus stays the outer wall.\n\t///\n\t/// THE WINDOW BOUNDS WHERE A LINE WANDERS, NOT WHERE IT MAY BE PUT. An answer transposes its\n\t/// call bodily (<see cref=\"AnswerOp.SequenceUp2\"/> takes it up two degrees), and that is a\n\t/// deliberate move rather than a walk drifting out of register \u2014 so <see cref=\"Answer\"/>\n\t/// reflects off the ambitus. Folding a sequence back into the call's window would flatten the\n\t/// one gesture in the tune whose whole point is that it goes somewhere else.\n\t///\n\t/// Authored like everything else here (there is no melodic corpus in this repo). The published\n\t/// pop-melody work that gives whole-song ambitus at around two octaves measures range on a\n\t/// rolling two-bar window for exactly this reason, but its figures are for a different roster\n\t/// and are not borrowed as a number \u2014 this is a judgement about a phrase being one gesture in\n\t/// one register, and <c>--stats</c> reports what the engine actually does with it.</summary>\n\tpublic const int PhraseSpan = 8;\n\n\t/// <summary>The note lengths a tune may be written in, in ticks: sixteenth, eighth, dotted\n\t/// eighth, quarter, dotted quarter, half. <see cref=\"Timing.TicksPerBeat\"/> is 48, so every one\n\t/// of them is exact and <see cref=\"Timing\"/> needs nothing \u2014 the same clean division the\n\t/// thirty-second work already proved.\n\t///\n\t/// The line every genre's weights are split on is the QUARTER: the first three are shorter than\n\t/// a beat and the last three are a beat or longer, which is what <c>move</c> leans on to make a\n\t/// verse sparser than its chorus without a second density mechanism.</summary>\n\tpublic static readonly int[] Lengths = { 12, 24, 36, 48, 72, 96 };\n\n\t/// <summary>Index of the first length that is a beat or longer.</summary>\n\tconst int LongFrom = 3;\n\n\t/// <summary>How a phrase answers itself. The answer used to not be DRAWN at all: it was the\n\t/// call's degrees minus one, every genre, every song, with a forced tonic on the end \u2014 so half\n\t/// of every tune in the engine was a mechanical transform of the other half.</summary>\n\tpublic enum AnswerOp\n\t{\n\t\t/// <summary>The call a step lower \u2014 the old behaviour, and still the heaviest weight in\n\t\t/// most genres because it is genuinely the commonest answer in this music.</summary>\n\t\tTranspose,\n\t\t/// <summary>The same line with a different landing: identical degrees, and the last two\n\t\t/// re-drawn to step home. What a chorus does.</summary>\n\t\tNewTail,\n\t\t/// <summary>The call restated a degree higher \u2014 a question answered with a bigger question,\n\t\t/// which still resolves because the last note is the tonic either way.</summary>\n\t\tSequenceUp,\n\t\t/// <summary>Restated two degrees higher.</summary>\n\t\tSequenceUp2,\n\t\t/// <summary>Mirrored about the call's first degree: where the call rose, the answer falls.\n\t\t/// </summary>\n\t\tInvert,\n\t}\n\n\tpublic static readonly AnswerOp[] Answers =\n\t{\n\t\tAnswerOp.Transpose, AnswerOp.NewTail, AnswerOp.SequenceUp, AnswerOp.SequenceUp2, AnswerOp.Invert,\n\t};\n\n\t/// <summary>How the CONSEQUENT opens \u2014 the one decision that makes a period a period.\n\t///\n\t/// A period is two call/answer pairs: an antecedent that leaves the line open and a consequent\n\t/// that closes it. Both pairs are built by exactly the machinery below; what varies is how much\n\t/// of the antecedent's call the consequent's call keeps. That is the classical taxonomy and it\n\t/// is also the whole variation budget \u2014 the answers repeat their own call's rhythm either way,\n\t/// so if the consequent's call does not move, nothing in the tune's second half is new.</summary>\n\tpublic enum PeriodShape\n\t{\n\t\t/// <summary>PARALLEL \u2014 the consequent restates the call note for note and differs only in\n\t\t/// how it answers. The commonest period in this music, and the one that makes the tune\n\t\t/// unmistakably one tune; it is also the least new material, which is why it is not the\n\t\t/// only shape.</summary>\n\t\tParallel,\n\t\t/// <summary>VARIED \u2014 the consequent keeps the call's rhythm and sings a fresh contour over\n\t\t/// it. The rhythm is what a listener remembers, so this reads as the same phrase said again\n\t\t/// differently rather than as a second idea.</summary>\n\t\tVaried,\n\t\t/// <summary>CONTRASTING \u2014 the consequent opens with a phrase of its own, rhythm and all.\n\t\t/// The departure, and the only shape that puts a second rhythm in the tune.</summary>\n\t\tContrasting,\n\t}\n\n\tpublic static readonly PeriodShape[] Shapes =\n\t{\n\t\tPeriodShape.Parallel, PeriodShape.Varied, PeriodShape.Contrasting,\n\t};\n\n\t/// <summary>Authored, not measured \u2014 there is no melodic corpus in this repo (see the note on\n\t/// <c>GenreProfile.Tune</c>) and this is a judgement about how much a tune may move under\n\t/// itself. It is one table rather than six because nothing found says a genre has an opinion\n\t/// about it; a genre that turns out to want one puts weights in <see cref=\"TuneVocab\"/>, the\n\t/// way <see cref=\"Answers\"/> already does.</summary>\n\tstatic readonly int[] ShapeWeights = { 4, 3, 3 };\n\n\t/// <summary>Where an ANTECEDENT lands: a chord tone that is not the tonic, which is what leaves\n\t/// the line open. The fifth is the half cadence proper and takes most of the weight; the third\n\t/// is the softer one. Landing home here would close the tune half way through it and make the\n\t/// consequent an appendix rather than an answer.</summary>\n\tstatic readonly int[] HalfCadence = { 4, 2 };\n\tstatic readonly int[] HalfCadenceWeights = { 3, 2 };\n\n\t/// <summary>The fewest bars a phrase may be. A period is four phrases, so a tune shorter than\n\t/// four of these is two phrases and no period \u2014 a one-bar \"phrase\" is a fragment, and four of\n\t/// them is a tune that restates itself every bar, which is the defect this exists to fix\n\t/// arriving from the other direction.</summary>\n\tpublic const int MinPhraseBars = 2;\n\n\t/// <summary>How many phrases a tune of <paramref name=\"bars\"/> bars is written in: four (a\n\t/// period) where they are long enough to be phrases, two (a plain call and answer) otherwise.\n\t/// </summary>\n\tpublic static int PhraseCount( int bars ) => bars >= 4 * MinPhraseBars ? 4 : 2;\n\n\t/// <summary>Where a tune may open \u2014 chord tones only, weighted toward the tonic and the fifth,\n\t/// with the octave reachable.</summary>\n\tstatic readonly int[] Opens = { 0, 2, 4, 7 };\n\tstatic readonly int[] OpenWeights = { 5, 3, 4, 2 };\n\n\t/// <summary>How far the phrase leans uphill at its start and downhill at its end \u2014 the MELODIC\n\t/// ARCH. Phrases in this music (and in every corpus anyone has counted) rise and then fall on\n\t/// average, and a plain random walk does not: it wanders, and the only thing that ever brought\n\t/// it home was the forced tonic on the last note, which is a landing with no approach to it.\n\t///\n\t/// 0 would be the old coin toss; 0.5 would make direction deterministic and turn every tune\n\t/// into the same hill. This is a lean on a draw, not a shape imposed on one.</summary>\n\tconst float Arch = 0.25f;\n\n\t/// <summary>How hard the line is pulled back toward the middle of its PHRASE WINDOW \u2014\n\t/// TESSITURA, the fact that a melody orbits a central pitch rather than diffusing across\n\t/// everything it is allowed to sing.\n\t///\n\t/// It is what actually keeps a tune off the range ends. <see cref=\"Reflect\"/> is a BACKSTOP: it\n\t/// stops a line parking at a boundary, but a walk with no centre still spends its time out\n\t/// there, and the arch makes that worse in the first half of every phrase by leaning uphill\n\t/// whatever the register already is. The two are different jobs and both are needed \u2014 this\n\t/// decides where the line lives, reflection decides what happens when it arrives at an edge\n\t/// anyway. The centre it pulls toward is the PHRASE's (<see cref=\"PhraseSpan\"/>), so a phrase\n\t/// orbits its own register rather than the middle of everything the tune may reach.</summary>\n\tconst float Centre = 0.30f;\n\n\t/// <summary>\n\t/// Draw a tune: <paramref name=\"bars\"/> bars built as a PERIOD where they are long enough for\n\t/// one, and as a plain call and answer where they are not.\n\t///\n\t/// A call and answer is a pair of phrases \u2014 the first states a shape and leaves it open, the\n\t/// second repeats that rhythm and resolves it home. That symmetry is most of what makes a line\n\t/// sound composed rather than generated, and a fresh random phrase every two bars never sounds\n\t/// like a tune however good the notes are.\n\t///\n\t/// A PERIOD IS TWO OF THOSE PAIRS AND SITS ABOVE THEM, NOT INSTEAD OF THEM. The antecedent\n\t/// (call, answer) lands on a chord tone that is not the tonic and so leaves the line open; the\n\t/// consequent (call, answer) opens from the antecedent \u2014 restating it, varying it, or departing\n\t/// from it (<see cref=\"PeriodShape\"/>) \u2014 and resolves home. That is what puts repetition at the\n\t/// whole tune's length and variation at a phrase's, instead of the binary shape the tune had\n\t/// before this: two phrases, one rhythm between them, looped to fill the section and repeated\n\t/// identically at every chorus.\n\t///\n\t/// THE RHYTHM REPEAT STAYS, WITHIN A PAIR. Varying an answer's rhythm stops its two phrases\n\t/// being heard as a question and an answer at all; the only rhythmic freedom an answer gets is\n\t/// where its last notes land, and that arrives through <see cref=\"AnswerOp.NewTail\"/> rather\n\t/// than through a second rhythm draw. A new rhythm enters a tune at the CONSEQUENT'S CALL or\n\t/// nowhere. The 100%-tonic ending stays too \u2014 that is not a defect to be varied away, it is\n\t/// what makes the thing a tune.\n\t/// </summary>\n\t/// <param name=\"v\">The genre's vocabulary \u2014 the note lengths it sings in, how often it rests,\n\t/// how often it leaps, and how it answers itself.</param>\n\t/// <param name=\"move\">How much this line moves relative to the genre's own table: 1 for a\n\t/// chorus, less for the sparser verse tune. It leans the length draw toward the long end rather\n\t/// than being a second density knob sitting beside the weights.</param>\n\t/// <param name=\"swung\">True where the song swings or shuffles. THE SIXTEENTH COMES OUT OF THE\n\t/// MENU: under a shuffle the beat's own subdivision IS the triplet, and Timing's warp puts a\n\t/// straight sixteenth at a third of the beat while the band's eighth-based figures sit on the\n\t/// beat and at two thirds. That is not syncopation, it is two grids at once, and it reads as\n\t/// the lead pushing against a band it does not line up with. A shuffled genre's melody moves in\n\t/// eighths and the shuffle does the subdividing.</param>\n\tpublic static Pattern Draw( Rng rng, int bars, int barTicks, in TuneVocab v, float move = 1f,\n\t\tbool swung = false )\n\t{\n\t\tint phrases = PhraseCount( bars );\n\t\tint phraseTicks = barTicks * Math.Max( 1, bars / phrases );\n\t\tvar ticks = new List<int>();\n\t\tvar degrees = new List<int>();\n\n\t\t// The genre's length table, leaned toward the long end for a verse. At move = 1 both\n\t\t// factors are 1 and the table is the genre's verbatim.\n\t\tvar weights = new int[Lengths.Length];\n\t\tfor ( int i = 0; i < weights.Length; i++ )\n\t\t{\n\t\t\tfloat w = v.LengthWeights[i] * (i < LongFrom ? move : 2f - move);\n\t\t\t// Anything that does not divide the eighth: the sixteenth AND the dotted eighth, which\n\t\t\t// lands mid-eighth for the same reason and was the half of this that was easy to miss.\n\t\t\tif ( swung && Lengths[i] % Timing.TicksPerEighth != 0 ) w = 0f;\n\t\t\tweights[i] = Math.Max( 0, (int)MathF.Round( w * 8f ) );\n\t\t}\n\n\t\t// \u2500\u2500 the antecedent \u2500\u2500\n\t\tvar callRhythm = DrawRhythm( rng, phraseTicks, weights, v );\n\t\tvar callDegrees = DrawContour( rng, callRhythm.Count, v );\n\t\tEmit( ticks, degrees, 0, callRhythm, callDegrees );\n\n\t\t// A HALF CADENCE IS WHAT MAKES THE CONSEQUENT NECESSARY. With a period the antecedent lands\n\t\t// on a chord tone that is not the tonic and stays open; with only two phrases there is\n\t\t// nothing after it, so it resolves the way it always did.\n\t\tint open = phrases == 4 ? HalfCadence[rng.WeightedIndex( HalfCadenceWeights )] : 0;\n\t\tEmit( ticks, degrees, phraseTicks, callRhythm, Answer( rng, callDegrees, v, open ) );\n\n\t\tif ( phrases == 4 )\n\t\t{\n\t\t\tvar shape = rng.PickWeighted( Shapes, ShapeWeights );\n\t\t\t// BOTH DRAWN FOR EVERY SHAPE, so swapping one shape for another does not shift the rest\n\t\t\t// of the tune's stream \u2014 the discipline PickOrNull keeps in the composer. A parallel\n\t\t\t// consequent pays for a phrase it does not sing.\n\t\t\tvar freshRhythm = DrawRhythm( rng, phraseTicks, weights, v );\n\t\t\tvar conRhythm = shape == PeriodShape.Contrasting ? freshRhythm : callRhythm;\n\t\t\tvar freshDegrees = DrawContour( rng, conRhythm.Count, v );\n\t\t\tvar conDegrees = shape == PeriodShape.Parallel ? callDegrees : freshDegrees;\n\n\t\t\tEmit( ticks, degrees, 2 * phraseTicks, conRhythm, conDegrees );\n\t\t\t// The consequent answers with its own operator \u2014 that is what a parallel period varies,\n\t\t\t// and it is the only thing it varies.\n\t\t\tEmit( ticks, degrees, 3 * phraseTicks, conRhythm, Answer( rng, conDegrees, v, 0 ) );\n\t\t}\n\n\t\t// A held final note, so the tune breathes before it comes round again.\n\t\treturn new Pattern( bars * barTicks, ticks.ToArray(), degrees.ToArray() );\n\t}\n\n\t/// <summary>Append one phrase's onsets (offset to <paramref name=\"at\"/>) and its degrees.\n\t/// </summary>\n\tstatic void Emit( List<int> ticks, List<int> degrees, int at, List<int> rhythm, List<int> pitches )\n\t{\n\t\tfor ( int i = 0; i < rhythm.Count; i++ ) { ticks.Add( at + rhythm[i] ); degrees.Add( pitches[i] ); }\n\t}\n\n\t/// <summary>One phrase's RHYTHM \u2014 the onsets, in ticks from the phrase's own start.\n\t///\n\t/// Rhythm first, and separately from the pitches: a melody's rhythm is what gets remembered,\n\t/// and drawing it on its own is what lets an answer repeat it exactly.\n\t///\n\t/// A REST IS AN OMITTED ONSET, not a cell. Leaving the tick out means the previous note's\n\t/// SpanTicks simply grows to cover the gap, and RenderTune's two-beat length cap turns the\n\t/// remainder into real silence \u2014 so rests cost the renderer nothing. A Melody.Rest CELL\n\t/// would be read as a DEGREE by RenderTune, which has no rest arm, and sung.</summary>\n\tstatic List<int> DrawRhythm( Rng rng, int phraseTicks, int[] weights, in TuneVocab v )\n\t{\n\t\tvar rhythm = new List<int>();\n\t\tfor ( int t = 0; t < phraseTicks; )\n\t\t{\n\t\t\t// THE PHRASE RE-ANCHORS TO THE BEAT, and without this the widened vocabulary is worse\n\t\t\t// than the two lengths it replaced. Free-running the accumulator over the menu means a\n\t\t\t// dotted eighth or a sixteenth shifts EVERY REMAINING NOTE of the phrase by a non-beat\n\t\t\t// amount, permanently \u2014 the line rotates against the bar and never comes back, which is\n\t\t\t// a 3-against-4 running for eight bars rather than a melody. It reads as the lead being\n\t\t\t// out of time with the band, because it is.\n\t\t\t//\n\t\t\t// The rule is the one a player reads off a stave: inside a beat you may only play what\n\t\t\t// fits the rest of it. So a dotted eighth is followed by a sixteenth, a sixteenth by\n\t\t\t// whatever fills the remaining three, and the next beat starts on the beat. Notes still\n\t\t\t// land on the \"and\" and on sixteenths \u2014 what they cannot do is drift.\n\t\t\tint inBeat = t % Timing.TicksPerBeat;\n\t\t\tint len;\n\t\t\tif ( inBeat == 0 ) len = Lengths[rng.WeightedIndex( weights )];\n\t\t\telse\n\t\t\t{\n\t\t\t\tint room = Timing.TicksPerBeat - inBeat;\n\t\t\t\tvar fits = new int[Lengths.Length];\n\t\t\t\tbool any = false;\n\t\t\t\tfor ( int i = 0; i < Lengths.Length; i++ )\n\t\t\t\t\tif ( Lengths[i] <= room ) { fits[i] = weights[i]; any |= weights[i] > 0; }\n\t\t\t\tlen = any ? Lengths[rng.WeightedIndex( fits )] : room;\n\t\t\t}\n\t\t\t// Never open the phrase on silence: a tune that starts by not being there has no shape\n\t\t\t// for the answer to repeat.\n\t\t\tif ( rhythm.Count == 0 || !rng.Chance( v.Rest ) ) rhythm.Add( t );\n\t\t\tt += len;\n\t\t}\n\t\t// A call of one note is not a call. Only reachable when every cell after the first drew a\n\t\t// rest, which is rare and still worth not shipping.\n\t\tif ( rhythm.Count < 2 ) rhythm.Add( phraseTicks / 2 );\n\t\treturn rhythm;\n\t}\n\n\t/// <summary>One phrase's CONTOUR \u2014 <paramref name=\"notes\"/> degrees relative to the key's tonic.\n\t///\n\t/// Three things shape it and none of them is a random walk: the arch (see <see cref=\"Arch\"/>),\n\t/// post-skip reversal (below), and reflection off the range ends instead of a clamp.\n\t///\n\t/// A phrase opens on a CHORD TONE \u2014 a melody that opens on the second or the seventh is a\n\t/// melody that starts by needing to resolve \u2014 weighted toward the tonic and the fifth, where\n\t/// far more tunes actually start, with the octave reachable. A uniform draw over three values\n\t/// is the sort of thing that shows up in a sweep as 33/33/33 and in a listen as \"they all start\n\t/// the same way\".</summary>\n\tstatic List<int> DrawContour( Rng rng, int notes, in TuneVocab v )\n\t{\n\t\tvar degrees = new List<int>( notes );\n\t\tint degree = Opens[rng.WeightedIndex( OpenWeights )];\n\t\t// THE PHRASE'S OWN WINDOW, drawn around the note the phrase opens on so that the opening\n\t\t// degree keeps its weighting and cannot land outside its own register. Where the window may\n\t\t// sit is what gives the tune its wider reach: two phrases an octave apart cover the ambitus\n\t\t// between them without either of them wandering.\n\t\tint loMin = Math.Max( DegreeMin, degree - (PhraseSpan - 1) );\n\t\tint loMax = Math.Min( degree, DegreeMax - (PhraseSpan - 1) );\n\t\tif ( loMax < loMin ) loMax = loMin;\n\t\tint lo = Math.Min( loMin + rng.Int( loMax - loMin + 1 ), DegreeMax );\n\t\tint hi = Math.Min( lo + PhraseSpan - 1, DegreeMax );\n\t\tint owed = 0;\n\t\tfor ( int i = 0; i < notes; i++ )\n\t\t{\n\t\t\tdegrees.Add( degree );\n\n\t\t\tbool leap = rng.Next() < v.Leap;\n\t\t\t// A leap is a third, a fourth or a fifth. It used to be a third and nothing else, in\n\t\t\t// every genre and every song \u2014 \"a leap\" was one interval wearing a general name.\n\t\t\tint size = leap ? 2 + rng.Int( 3 ) : 1;\n\t\t\tint sign;\n\t\t\tif ( owed != 0 )\n\t\t\t{\n\t\t\t\t// POST-SKIP REVERSAL: a melody that jumps comes back. It is one of the most robust\n\t\t\t\t// findings there is about how tunes are actually written, and it is also what makes\n\t\t\t\t// a leap read as a gesture rather than as the line relocating.\n\t\t\t\tsign = owed;\n\t\t\t\towed = 0;\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tfloat u = notes < 2 ? 0.5f : i / (float)(notes - 1);\n\t\t\t\t// Where in the PHRASE'S window this note sits, \u22121 at the bottom and +1 at the top.\n\t\t\t\tfloat mid = (lo + hi) / 2f, half = Math.Max( 1f, (hi - lo) / 2f );\n\t\t\t\tfloat pos = (degree - mid) / half;\n\t\t\t\tsign = rng.Chance( Math.Clamp( 0.5f + Arch * (1f - 2f * u) - Centre * pos, 0.05f, 0.95f ) )\n\t\t\t\t\t? 1 : -1;\n\t\t\t}\n\t\t\tif ( leap ) owed = -sign;\n\t\t\tdegree = Reflect( degree + sign * size, lo, hi );\n\t\t}\n\t\treturn degrees;\n\t}\n\n\t/// <summary>ANSWER a call: the same rhythm, the call's degrees put through one of the genre's\n\t/// <see cref=\"AnswerOp\"/>s, landing on <paramref name=\"last\"/> \u2014 the tonic where this phrase\n\t/// closes the tune, an open chord tone where it is an antecedent handing over to a consequent.\n\t/// Every operator answers the same question and every one of them lands in the same place.\n\t/// </summary>\n\tstatic List<int> Answer( Rng rng, List<int> call, in TuneVocab v, int last )\n\t{\n\t\tvar op = rng.PickWeighted( Answers, v.AnswerWeights );\n\t\t// Drawn for every operator, so swapping one for another does not shift the rest of the\n\t\t// tune's stream \u2014 the same discipline PickOrNull keeps in the composer.\n\t\tint approach = rng.Chance( 0.65f ) ? 1 : -1;\n\t\tint n = call.Count;\n\t\tint first = call[0];\n\t\tvar answer = new List<int>( n );\n\t\tfor ( int i = 0; i < n; i++ )\n\t\t{\n\t\t\tif ( i == n - 1 ) { answer.Add( Reflect( last ) ); continue; }\n\t\t\tint d = op switch\n\t\t\t{\n\t\t\t\tAnswerOp.NewTail => i == n - 2 ? last + approach : call[i],\n\t\t\t\tAnswerOp.SequenceUp => call[i] + 1,\n\t\t\t\tAnswerOp.SequenceUp2 => call[i] + 2,\n\t\t\t\tAnswerOp.Invert => 2 * first - call[i],\n\t\t\t\t_ => call[i] - 1,\n\t\t\t};\n\t\t\tanswer.Add( Reflect( d ) );\n\t\t}\n\t\treturn answer;\n\t}\n\n\t/// <summary>Fold a degree back inside the singable range by REFLECTING off its ends.\n\t///\n\t/// A clamp is sticky: a line that reaches a boundary and keeps stepping outward parks there,\n\t/// which is where 10\u201318% of every tune's notes sat and where most of its repeated adjacent\n\t/// notes came from \u2014 the two are the same defect seen from either side. Reflection keeps the\n\t/// range (that is what makes a tune singable) and turns the wall into a turn.\n\t///\n\t/// Off the AMBITUS \u2014 the outer wall, which is what a transposed answer folds against.</summary>\n\tinternal static int Reflect( int degree ) => Reflect( degree, DegreeMin, DegreeMax );\n\n\t/// <summary>Fold a degree back inside an arbitrary window by reflecting off its ends \u2014 the\n\t/// walk inside one phrase uses its own (<see cref=\"PhraseSpan\"/>).</summary>\n\tinternal static int Reflect( int degree, int lo, int hi )\n\t{\n\t\tfor ( int guard = 0; guard < 8 && (degree < lo || degree > hi); guard++ )\n\t\t{\n\t\t\tif ( degree < lo ) degree = 2 * lo - degree;\n\t\t\tif ( degree > hi ) degree = 2 * hi - degree;\n\t\t}\n\t\treturn Math.Clamp( degree, lo, hi );\n\t}\n}\n\n// The tune, and how a bar of it is played. Part of the MusicGen engine \u2014 see MusicGen.cs.\n\npublic sealed partial class MusicGen\n{\n\t// The song's tunes, drawn once per song off their own streams (so having them shifts nothing\n\t// else in the composition) and keyed by SECTION TYPE. The chorus tune is the hook: identical\n\t// every chorus, which is the whole reason a chorus reads as one. The verse tune is a second,\n\t// sparser line \u2014 same song, different words.\n\tPattern _chorusTune, _verseTune;\n\n\t// How long one PHRASE of them is. A diagnostic wanting to read a tune phrase by phrase cannot\n\t// re-derive this without re-deciding the period, which is the re-implementation PlanTrace\n\t// exists to avoid \u2014 so the composer writes it down.\n\tint _tunePhraseTicks;\n\n\t/// <summary>One phrase of the song's tunes, in ticks (diagnostics \u2014 see <see cref=\"Melody\"/>).\n\t/// </summary>\n\tinternal int TunePhraseTicks => _tunePhraseTicks;\n\n\t/// <summary>The tune this section sings, or null where the section is not a place for one:\n\t/// a solo is where the genre's lead grammar improvises, an intro is a build-in, and the\n\t/// ending has already resolved.</summary>\n\tPattern TuneFor( Section s ) => !SectionSingsTune( s ) ? null\n\t\t: s == Section.Chorus ? _chorusTune : _verseTune;\n\n\t/// <summary>Whether a section TYPE is a place for a tune at all. Static because it is a\n\t/// property of the form rather than of a drawn song \u2014 which is what lets a form be checked for\n\t/// putting its feel changes somewhere the melody can contrast with them.</summary>\n\tinternal static bool SectionSingsTune( Section s ) =>\n\t\ts is Section.Chorus or Section.Verse or Section.PreChorus or Section.Bridge;\n\n\t/// <summary>Draw the song's tunes \u2014 one for choruses, a sparser one for verses. Every genre\n\t/// gets both: \"riff-led\" does not mean melody-free, and metal verses with no tune left four\n\t/// and eight bar holes where the lead simply did not play.</summary>\n\tvoid DrawTunes( int barTicks, bool swung )\n\t{\n\t\t// The vocabulary is the GENRE's (GenreProfile.Tune). It used to be a switch on _prof.Lead\n\t\t// right here, which is the `if ( _genre == \u2026 )` smell one level removed: two genres sharing\n\t\t// a LeadStyle got the same tune vocabulary, and where their densities matched the draws\n\t\t// agreed and the tunes came back identical.\n\t\t// The tune is a WHOLE NUMBER OF HARMONIC CYCLES \u2014 the bars it takes the progression to come\n\t\t// round (ChordBars x the progression's length), capped at eight. A four-bar tune over an\n\t\t// eight-bar cycle states itself twice, and the second statement lands over different\n\t\t// chords than it was written against: same notes, different harmony, which is exactly the\n\t\t// \"the lead clashes with the backing\" it sounds like. Matching the cycle means every\n\t\t// repetition sits over the changes it was drawn for.\n\t\tint cycle = Math.Clamp( _chordBars * _prog.Length, 2, 8 );\n\t\t// A PERIOD NEEDS FOUR PHRASES, AND A WHOLE NUMBER OF CYCLES IS STILL ALIGNED TO THE CHANGES.\n\t\t// The clamp above is not about length, it is about a tune's statements landing over the\n\t\t// chords they were drawn against \u2014 and a tune of exactly two cycles does, bar for bar. So a\n\t\t// genre whose cycle is short doubles the tune rather than being stuck with two phrases: punk\n\t\t// and pop (ChordBars 1 x a four-chord progression) went from a 4-bar tune whose rhythmic cell\n\t\t// was 2 bars, stated twice and looped, to an 8-bar period. Eight is the ceiling because a\n\t\t// section is eight bars \u2014 a tune longer than the section it is sung in never finishes.\n\t\tint bars = cycle;\n\t\twhile ( bars * 2 <= 8 ) bars *= 2;\n\t\t_tunePhraseTicks = barTicks * bars / Melody.PhraseCount( bars );\n\t\t// THE GENRE IS IN THE TUNE'S STREAM, and it is the only stream it is in.\n\t\t//\n\t\t// Without it the genre reached Draw through nothing but `density` and `leap`, so where two\n\t\t// genres' densities were close the draws mostly agreed and the tunes came back\n\t\t// BYTE-IDENTICAL: over 500 songs, rock and country sang the same melody 53% of the time and\n\t\t// punk and pop 52%. Same n, different key, different kit, literally the same tune \u2014 which is\n\t\t// most of why the roster read as one band.\n\t\t//\n\t\t// The SONG stream (ComposePlan's `new Rng( _tag )`) still has no genre in it, and that is a\n\t\t// feature rather than an oversight: genre 0 and genre 3 at the same tag:n share the root\n\t\t// note, the pan, the ride preference and the whole kit draw, so the same song in two genres\n\t\t// is a thing the toy can do. That is worth more than the variation putting the genre there\n\t\t// would buy, and it is why the genre goes in the TUNE streams and nowhere else.\n\t\t//\n\t\t// IT IS A DIFFERENT DRAW, NOT A GUARANTEED DIFFERENT TUNE, and that distinction is the\n\t\t// point. Two genres landing on a similar melody at one seed is the toy doing what it is\n\t\t// for; what was wrong before was that they landed there RELIABLY, off a stream that could\n\t\t// not tell them apart. Nothing here should ever grow into machinery that forces two genres\n\t\t// to diverge \u2014 the collision rate is something `--stats` reports, not something the engine\n\t\t// enforces.\n\t\t_chorusTune = Melody.Draw( new Rng( $\"{_tag}:tune:{_genre}:chorus\" ), bars, barTicks, _prof.Tune, 1f, swung );\n\t\t// The verse tune is the same vocabulary, sung with fewer notes in it \u2014 same song,\n\t\t// different words.\n\t\t_verseTune = Melody.Draw( new Rng( $\"{_tag}:tune:{_genre}:verse\" ), bars, barTicks, _prof.Tune, 0.8f, swung );\n\t}\n\n\t/// <summary>Play one bar of the section's tune.\n\t///\n\t/// Degrees are relative to the KEY, so the tune keeps its shape as the chords move. What\n\t/// keeps it consonant is resolution on the strong beats only: a note landing on a beat is\n\t/// pulled to the nearest tone of the bar's chord, while the notes between beats are free to\n\t/// pass through. Snapping everything would rewrite the tune chord by chord \u2014 which is\n\t/// exactly the \"no tune, just an improvisation over the changes\" this replaces.</summary>\n\tvoid RenderTune( Pattern tune, int barTick, int barTicks, int chord, Rng rng, Rng exprRng )\n\t{\n\t\tint melBase = LeadBase();\n\t\tvar tones = ChordDegrees( chord );\n\t\tbool guitarLead = !_hornLead;\n\t\tfloat amp = (guitarLead ? _c.LeadGtrVol * _c.LeadGtrBalance : _c.MelodyVol * _c.MelodyBalance)\n\t\t\t* _midMul;\n\t\tfloat drive = guitarLead ? _c.LeadGtrDrive : _c.MelodyDrive;\n\t\tvar ex = guitarLead ? Expr( \"LEAD GTR\" ) : Expr( \"LEAD\" );\n\t\tint prevMidi = NoPrev;\n\n\t\t// A SECTION SHORTER THAN THE TUNE SINGS THE TUNE'S END, not its beginning. A four-bar\n\t\t// pre-chorus over an eight-bar tune stated the call and was cut off by the chorus before\n\t\t// the answer ever arrived \u2014 a phrase interrupted by the next phrase, which is what \"two\n\t\t// ideas at once\" sounds like. Pulling the anchor back lands the tune's resolution exactly\n\t\t// on the section's last bar, which is what a pre-chorus is for.\n\t\tint anchor = _sectionTicks > 0 && _sectionTicks < tune.LengthTicks\n\t\t\t? _sectionTick - (tune.LengthTicks - _sectionTicks)\n\t\t\t: _sectionTick;\n\t\t// THE TUNE IS EXEMPT FROM THE SECTION'S FEEL, and that exemption IS half/double time.\n\t\t// Part.Feel is the RHYTHM SECTION's pattern rate: when a section halves or doubles, the\n\t\t// band changes rate underneath a vocal that stays exactly where it was \u2014 that contrast is\n\t\t// the entire gesture, and it is what makes a double-time chorus lift rather than sound\n\t\t// like the tape sped up. Scaling the hook by the same multiplier deletes the gesture and\n\t\t// leaves only a faster song. So the tune slices at the nominal rate; every other voice\n\t\t// (comp, keys, bass, horns, kit) reads _feel.\n\t\tvar sung = tune.Slice( barTick, barTick + barTicks, anchor );\n\t\tTrace?.Add( TraceVoice.Tune, sung );\n\t\tforeach ( var h in sung )\n\t\t{\n\t\t\tint degree = h.Value;\n\t\t\tint len = Math.Min( h.SpanTicks, Timing.TicksPerBeat * 2 );\n\t\t\tbool onBeat = (h.Tick - _barTick) % Timing.TicksPerBeat == 0;\n\n\t\t\t// What resolves is the note the ear has TIME to hear against the chord: anything on a\n\t\t\t// beat, and anything held for a beat or more. A quick note between beats is a passing\n\t\t\t// tone and is left alone \u2014 that is the difference between a melody and an arpeggio.\n\t\t\t// (Snapping only the on-beat notes left long off-beat non-chord tones ringing over the\n\t\t\t// backing for up to two beats, which is what a clash sounds like.)\n\t\t\tbool resolve = onBeat || len >= Timing.TicksPerBeat;\n\t\t\tif ( resolve ) degree = NearestChordTone( tones, degree );\n\t\t\tint midi = ScaleMidi( melBase, degree );\n\t\t\t// The degree snap chose WHICH chord tone; this puts the note on the pitch the chord\n\t\t\t// actually sounds, which is not the same thing on every degree (see NearestSoundingTone).\n\t\t\tif ( resolve ) midi = NearestSoundingTone( midi, chord, h.Tick );\n\t\t\t// Where this note sits in the TUNE, which is the phrase a bend leans into. The tune's\n\t\t\t// own length is the cycle, so this is the same 0..1 whatever bar the section is on.\n\t\t\tfloat pu = ((h.Tick - anchor) % tune.LengthTicks + tune.LengthTicks)\n\t\t\t\t\t% tune.LengthTicks / (float)tune.LengthTicks;\n\t\t\tvar vc = Roll( ex, midi, prevMidi, exprRng, (float)_time.SpanSeconds( h.Tick, len ),\n\t\t\t\t\tBendBias( len, pu ) );\n\t\t\tprevMidi = midi;\n\t\t\tRenderLeadNote( _time.TickToSample( h.Tick ), _time.SpanSamples( h.Tick, len * 0.92 ),\n\t\t\t\tmidi, amp * NoteGain( h.Vel ), _time.SpanSeconds( h.Tick, len ) * 0.8,\n\t\t\t\tdrive, vc );\n\n\t\t\t// The genre's own hand on the same tune: country punctuates it with double-stops, metal\n\t\t\t// runs between its notes. The line is the same either way \u2014 this is ORNAMENT, not a\n\t\t\t// different melody, which is the difference between a genre playing a song and a genre\n\t\t\t// having its own song. Ornament also means occasional: harmonising every long note in\n\t\t\t// parallel thirds replaces the melody with a two-note chord (see EmitDoubleStop).\n\t\t\tif ( _prof.Lead == LeadStyle.DoubleStop && len >= Timing.TicksPerEighth * 2\n\t\t\t\t&& rng.Chance( DoubleStopChance ) )\n\t\t\t\tEmitDoubleStop( h.Tick, len, degree, amp * NoteGain( h.Vel ) );\n\t\t\telse if ( _prof.Lead == LeadStyle.Shred && len >= Timing.TicksPerBeat && rng.Chance( 0.18f ) )\n\t\t\t\tfor ( int k = 1; k <= 3; k++ )\n\t\t\t\t{\n\t\t\t\t\tint m2 = ScaleMidi( melBase, degree + k );\n\t\t\t\t\tRenderLeadNote( _time.EvenSpan( h.Tick + len / 2, len / 2, (k - 1) / 3.0 ),\n\t\t\t\t\t\t_time.SpanSamples( h.Tick, len / 8.0 ), m2, amp * 0.8f * NoteGain( h.Vel ),\n\t\t\t\t\t\t_time.SpanSeconds( h.Tick, len / 8.0 ) * 0.8, drive, vc );\n\t\t\t\t}\n\t\t}\n\t}\n}\n"
        },
        {
            "Ident": "gamah.skafinity",
            "Path": "Code/UI/SkafinityTheme.cs",
            "FileName": "SkafinityTheme.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 341414,
            "IsPrivate": false,
            "Code": "using Sandbox;\n\nnamespace Skafinity;\n\n/// <summary>\n/// Runtime palette for <see cref=\"SkafinityMusicPanel\"/>. The whole palette derives from one\n/// hue, so a consuming game retints the board by setting a single colour:\n/// <code>SkafinityTheme.Accent = Color.Parse( myAccentHex );</code>\n/// Leave it unset and the board is neutral gray-on-black, which is what a drop-in library\n/// should look like \u2014 colour is the consumer's call, not this library's.\n/// </summary>\n/// <remarks>\n/// s&amp;box-only: this is UI, so it lives outside <c>Code/Engine/</c> (which stays\n/// framework-free for the wasm build).\n///\n/// SCSS variables are compile-time, so a vendored copy could only be re-themed by editing it \u2014\n/// which is exactly what the house rule against patching a vendored library forbids. The panel\n/// therefore binds these as inline <c>style=</c> values, the same way rotaliate/gambit's wall\n/// boards bind <c>WallTheme</c>; <c>SkafinityMusicPanel.razor.scss</c> keeps the non-colour\n/// tokens (layout, fonts, radii) and every border.\n/// </remarks>\npublic static class SkafinityTheme\n{\n\t// Unset = neutral. A mid gray rather than a dark one: the palette below scales DOWN for the\n\t// fills and UP toward white for the text, so the hue it starts from has to sit between them\n\t// for both ends to land \u2014 a near-black accent gives invisible tick fills.\n\tstatic readonly Color NeutralAccent = Color.Parse( \"#7a7a7a\" ) ?? Color.White;\n\n\t/// <summary>The hue the whole palette derives from. Null (the default) = neutral gray/black.\n\t/// Set it once at startup, or whenever your game's own theme changes \u2014 the panel folds this\n\t/// into its build hash, so a change re-renders the board.</summary>\n\tpublic static Color? Accent { get; set; }\n\n\tstatic Color Hue => Accent ?? NeutralAccent;\n\n\t// Derived palette. The factors are WallTheme's, so a game that passes its wall accent in gets\n\t// the same board it already has elsewhere \u2014 and #2f9450 reproduces the panel's original\n\t// hardcoded green.\n\t/// <summary>Board background (near-black tint of the accent).</summary>\n\tpublic static string Bg => Rgb( Scale( Hue, 0.09f ) );\n\t/// <summary>Button / cell / queue-entry fill \u2014 deeper than the board.</summary>\n\tpublic static string Cell => Rgb( Scale( Hue, 0.04f ) );\n\t/// <summary>Filled ticks and selected volume cells.</summary>\n\tpublic static string CellFill => Rgb( Scale( Hue, 0.6f ) );\n\t/// <summary>A wash of <see cref=\"CellFill\"/> \u2014 a cached queue entry.</summary>\n\tpublic static string CellFillSoft => Rgba( Scale( Hue, 0.6f ), 0.25f );\n\t/// <summary>The accent itself: progress bars, \"on\" text, status lines.</summary>\n\tpublic static string AccentCss => Rgb( Hue );\n\t/// <summary>Background of an active toggle / the current queue entry.</summary>\n\tpublic static string AccentBg => Rgba( Hue, 0.2f );\n\t/// <summary>Primary text (button labels).</summary>\n\tpublic static string Text => Rgba( Mix( Hue, 0.8f ), 0.9f );\n\t/// <summary>Labels and secondary lines.</summary>\n\tpublic static string TextDim => Rgba( Mix( Hue, 0.72f ), 0.7f );\n\n\t// \u2500\u2500 The same palette, as colours \u2500\u2500\n\t// Everything above is a CSS string because the razor spends it in a style= attribute. Panels this\n\t// library builds and styles from C# (SkafinitySlider) need the value itself, so the two that one\n\t// needs are exposed here as well. Same source, so they cannot drift.\n\t/// <summary>The accent itself. What a slider's fill and thumb are painted with.</summary>\n\tpublic static Color AccentColor => Hue;\n\t/// <summary>A slider's trough \u2014 neutral, like the stylesheet's borders, so it reads against any\n\t/// accent a host passes in.</summary>\n\tpublic static Color TrackColor => new Color( 1f, 1f, 1f, 0.18f );\n\n\t// Component-wise helpers (avoid relying on Color operators), matching the sibling repos' style.\n\tstatic Color Scale( Color c, float f ) => new Color( c.r * f, c.g * f, c.b * f );\n\tstatic Color Mix( Color c, float towardWhite ) => new Color(\n\t\tc.r + ( 1f - c.r ) * towardWhite,\n\t\tc.g + ( 1f - c.g ) * towardWhite,\n\t\tc.b + ( 1f - c.b ) * towardWhite );\n\n\tstatic string Rgb( Color c ) => $\"rgb({To255( c.r )},{To255( c.g )},{To255( c.b )})\";\n\tstatic string Rgba( Color c, float a ) => $\"rgba({To255( c.r )},{To255( c.g )},{To255( c.b )},{a})\";\n\tstatic int To255( float v ) => (int)System.MathF.Round( System.Math.Clamp( v, 0f, 1f ) * 255f );\n}\n"
        },
        {
            "Ident": "gamah.skafinity",
            "Path": "Engine/Arrange.cs",
            "FileName": "Arrange.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 341414,
            "IsPrivate": false,
            "Code": "using System;\nusing System.Collections.Generic;\n\nnamespace Skafinity;\n\n/// <summary>Which cells of a bar a voice is ALLOWED to play on. This is what keeps ska's skank\n/// offbeat by RULE rather than by table \u2014 the arranger may move an onset, but not off the class\n/// the genre's technique lives in, so a skank cannot drift onto the downbeat however loud the\n/// accent grid is there.\n///\n/// It is the single property that has to survive the whole arranger, and it is the reason the\n/// arranger cannot simply write onsets wherever the accent grid is loud. A genre's identity is\n/// WHERE it plays; its arrangement is which of those places it uses this time.</summary>\nenum CellClass\n{\n\t/// <summary>Every sixteenth \u2014 metal's gallop, and anything that subdivides freely.</summary>\n\tSixteenths,\n\t/// <summary>Every eighth \u2014 the rock riff, the pop pad.</summary>\n\tEighths,\n\t/// <summary>The beats only \u2014 punk's downstrokes.</summary>\n\tDownbeats,\n\t/// <summary>The \"and\" of each beat only \u2014 the ska skank and country's chick.</summary>\n\tOffbeats,\n}\n\n/// <summary>\n/// THE SECTION'S RHYTHMIC SKELETON \u2014 what every part is written against.\n///\n/// Before this, every voice picked its figure from its own small authored table and no voice knew\n/// what any other was playing. The single exception was the riff-doubling bass, and it was the\n/// only lockup in the engine that was not a coincidence: pop's pad landed on the kick 100% of the\n/// time and ska's skank 1%, and neither number was decided by anyone.\n///\n/// The skeleton is deliberately DERIVED, not drawn. The kit is table-driven and its grooves are\n/// fitted to a played corpus (see <see cref=\"DrumGroove\"/>), so the drums are the measured\n/// reference the arranger writes against rather than another client of it \u2014 which is why the\n/// accent grid comes off the kick and the snare and the genre's own measured accent weights, and\n/// why nothing here rolls a die to decide where the section leans.\n///\n/// Everything is on the section's own SIXTEENTH grid. Occupancy fills in as each part is placed,\n/// so a voice arranged later can see what the ones before it took \u2014 that is what \"one authority\n/// arranges every part at once\" amounts to in practice.\n/// </summary>\nsealed class Skeleton\n{\n\tpublic const int CellTicks = Timing.TicksPerEighth / 2;\n\n\t/// <summary>First tick of the section, and how many sixteenth cells long it is.</summary>\n\tpublic readonly int StartTick, Cells;\n\n\t/// <summary>Cells per bar \u2014 the seam and allowed-class tests are per bar.</summary>\n\tpublic readonly int BarCells;\n\n\t/// <summary>Where the section leans, 0..1, off the groove's kick and snare and the genre's\n\t/// measured accent weights.</summary>\n\tpublic readonly float[] Accent;\n\n\t/// <summary>Where the kick lands. The bass's lock reads this directly rather than the accent\n\t/// grid \u2014 \"agrees with the kick\" is a different claim from \"is loud in the same place\".</summary>\n\tpublic readonly bool[] Kick;\n\n\t/// <summary>Phrase ends: every four bars, and the section's last bar. Where a band converges.\n\t/// </summary>\n\tpublic readonly bool[] Seam;\n\n\t/// <summary>Where the tune has an onset, and where it is holding a note through.</summary>\n\tpublic readonly bool[] TuneOn, TuneHold;\n\n\t/// <summary>What the parts placed so far have taken. Mutated as the arranger works down the\n\t/// voices, which is the whole point of arranging them in one pass.</summary>\n\tpublic readonly bool[] Taken;\n\n\tpublic Skeleton( int startTick, int ticks, int barTicks )\n\t{\n\t\tStartTick = startTick;\n\t\tCells = Math.Max( 1, ticks / CellTicks );\n\t\tBarCells = Math.Max( 1, barTicks / CellTicks );\n\t\tAccent = new float[Cells];\n\t\tKick = new bool[Cells];\n\t\tSeam = new bool[Cells];\n\t\tTuneOn = new bool[Cells];\n\t\tTuneHold = new bool[Cells];\n\t\tTaken = new bool[Cells];\n\t}\n\n\t/// <summary>The cell a SONG tick falls in, or \u22121 outside the section.</summary>\n\tpublic int CellAt( int tick )\n\t{\n\t\tint c = (tick - StartTick) / CellTicks;\n\t\treturn c < 0 || c >= Cells ? -1 : c;\n\t}\n\n\t/// <summary>Whether a cell is one this voice's technique may play on.</summary>\n\tpublic bool Allows( CellClass cls, int cell )\n\t{\n\t\tint inBar = cell % BarCells;\n\t\treturn cls switch\n\t\t{\n\t\t\tCellClass.Sixteenths => true,\n\t\t\tCellClass.Eighths => inBar % 2 == 0,\n\t\t\tCellClass.Downbeats => inBar % 4 == 0,\n\t\t\t_ => inBar % 4 == 2,\n\t\t};\n\t}\n}\n\n/// <summary>How a voice arranges itself against the skeleton: where it may play, and what it is\n/// pulled toward or pushed away from. The parameters are the genre's (see\n/// <see cref=\"GenreProfile\"/>) because they say HOW a part behaves, never WHAT it plays \u2014 the\n/// figure is still the genre's own authored gesture.</summary>\nreadonly struct ArrangeRole\n{\n\tpublic readonly CellClass Cells;\n\t/// <summary>Pull toward cells the kick plays \u2014 how hard this voice locks to the drums.</summary>\n\tpublic readonly float Kick;\n\t/// <summary>Push away from cells another part has already taken, and from the tune's landings.\n\t/// </summary>\n\tpublic readonly float Complement;\n\t/// <summary>Pull toward phrase seams, where a band converges.</summary>\n\tpublic readonly float Seam;\n\n\t/// <summary>What an ADDED onset plays, where the voice's vocabulary says an addition is one\n\t/// particular thing rather than \"another of whatever was before it\". The snare is the case: a\n\t/// drummer filling in around a backbeat adds GHOSTS, and copying the previous cell would let a\n\t/// bar acquire a second struck backbeat \u2014 which is the one thing about a snare part a listener\n\t/// would notice immediately. <see cref=\"NoValue\"/> keeps the copy-the-previous default.\n\t/// </summary>\n\tpublic readonly int AddValue;\n\n\tpublic const int NoValue = int.MinValue;\n\n\tpublic ArrangeRole( CellClass cells, float kick, float complement, float seam,\n\t\tint addValue = NoValue )\n\t{ Cells = cells; Kick = kick; Complement = complement; Seam = seam; AddValue = addValue; }\n}\n\n// The arranger. Part of the MusicGen engine \u2014 see MusicGen.cs.\n\npublic sealed partial class MusicGen\n{\n\t/// <summary>The current section's skeleton, published in <c>RenderSection</c> alongside\n\t/// <c>_energy</c> / <c>_feel</c> / <c>_keyShift</c> \u2014 the same mechanism, so a voice reads it\n\t/// the way it reads those.</summary>\n\tSkeleton _skeleton;\n\n\t/// <summary>Build the section's skeleton, then arrange each part against it in turn.\n\t///\n\t/// ORDER IS THE DESIGN: the bass goes first because its role is to agree with the kick, which\n\t/// is already decided; the comp then sees the bass and the tune and can complement them; the\n\t/// keys see all three. Arranging them independently against a fixed grid would give every voice\n\t/// the same answer, which is the failure mode this whole phase has to avoid.</summary>\n\tvoid PlanArrangement( in Part part, int sectionTick, int barTicks, Pattern tune, string bk )\n\t{\n\t\tvar sk = new Skeleton( sectionTick, _sectionTicks, barTicks );\n\t\t_skeleton = sk;\n\t\t_kitArranged = false;\n\n\t\t// \u2500\u2500 the seams \u2500\u2500\n\t\t// A phrase ends every four bars, and the section's last bar is one whatever its length.\n\t\tfor ( int bar = 4; bar * sk.BarCells < sk.Cells; bar += 4 )\n\t\t\tsk.Seam[bar * sk.BarCells - 1] = true;\n\t\tif ( sk.Cells > 0 ) sk.Seam[sk.Cells - 1] = true;\n\n\t\t// \u2500\u2500 the tune's occupancy \u2500\u2500\n\t\t// The tune is written first and everything else is written against it, which is the order a\n\t\t// song is actually made in. Note that the tune is exempt from the section's feel, so it is\n\t\t// sliced at the nominal rate here exactly as RenderTune slices it.\n\t\tif ( tune != null )\n\t\t{\n\t\t\tint anchor = _sectionTicks > 0 && _sectionTicks < tune.LengthTicks\n\t\t\t\t? sectionTick - (tune.LengthTicks - _sectionTicks) : sectionTick;\n\t\t\tforeach ( var h in tune.Slice( sectionTick, sectionTick + _sectionTicks, anchor ) )\n\t\t\t{\n\t\t\t\tint c = sk.CellAt( h.Tick );\n\t\t\t\tif ( c < 0 ) continue;\n\t\t\t\tsk.TuneOn[c] = true;\n\t\t\t\tint held = Math.Min( h.SpanTicks, Timing.TicksPerBeat * 2 ) / Skeleton.CellTicks;\n\t\t\t\tfor ( int k = 1; k < held && c + k < sk.Cells; k++ ) sk.TuneHold[c + k] = true;\n\t\t\t}\n\t\t}\n\n\t\t// \u2500\u2500 who goes first \u2500\u2500\n\t\t// The skeleton the band writes against is the kit's accents, plus the genre's metric\n\t\t// weights, plus the phrase seams, plus the tune. THREE OF THOSE FOUR DO NOT NEED THE KIT,\n\t\t// which is what makes both orderings one mechanism rather than two.\n\t\t//\n\t\t// KIT LEADS: the kit arranges against seams, metre and tune; its accents then go on the\n\t\t// grid; the band follows. KIT FOLLOWS: the band writes against a grid with no kit on it and\n\t\t// the kit arranges last, against what the band actually took \u2014 a drummer playing to the\n\t\t// riff. It is not a coin flip dressed up: a leading kit is most punk and most rock, a\n\t\t// following kit is riff-led metal and a great deal of programmed pop.\n\t\tvar rng = new Rng( $\"{_tag}:arr:{bk}\" );\n\t\tif ( _kitLeads ) { ArrangeKit( sk, bk ); KitAccents( sk ); }\n\t\telse KitAccents( sk );\n\n\t\tArrangeBand( part, sk, rng );\n\n\t\tif ( !_kitLeads ) { ArrangeKit( sk, bk ); KitAccents( sk ); }\n\t}\n\n\t/// <summary>The accent grid, off the kit and the genre's own measured weights.\n\t///\n\t/// WITH NO KIT ON THE GRID YET the cells carry the metre alone \u2014 which is the honest reading of\n\t/// \"the band writes against seams, metre and tune\", and not a floor invented to keep the number\n\t/// non-zero: it is exactly this formula with the kit's occupancy taken as flat.</summary>\n\tvoid KitAccents( Skeleton sk )\n\t{\n\t\tbool haveKit = _kitArranged;\n\t\tfor ( int c = 0; c < sk.Cells; c++ ) { sk.Accent[c] = 0f; sk.Kick[c] = false; }\n\n\t\tif ( haveKit )\n\t\t{\n\t\t\tforeach ( var h in _kickFig.Slice( sk.StartTick, sk.StartTick + _sectionTicks, sk.StartTick, _feel ) )\n\t\t\t{\n\t\t\t\tint c = sk.CellAt( h.Tick );\n\t\t\t\tif ( c < 0 ) continue;\n\t\t\t\tsk.Kick[c] = true;\n\t\t\t\tsk.Accent[c] += h.Vel;\n\t\t\t}\n\t\t\tforeach ( var h in _snareFig.Slice( sk.StartTick, sk.StartTick + _sectionTicks, sk.StartTick, _feel ) )\n\t\t\t{\n\t\t\t\tint c = sk.CellAt( h.Tick );\n\t\t\t\tif ( c >= 0 ) sk.Accent[c] += h.Value == DrumGroove.Ghost ? 0.3f : h.Vel;\n\t\t\t}\n\t\t}\n\n\t\t// The genre's own measured accent weights on top: a country bar leans on its offbeat and a\n\t\t// metal bar is deliberately flat, and that is a property of the genre rather than of the\n\t\t// groove it drew.\n\t\tfor ( int c = 0; c < sk.Cells; c++ )\n\t\t{\n\t\t\tint inBar = c % sk.BarCells;\n\t\t\tfloat metric = inBar == 0 ? _prof.AccentDown\n\t\t\t\t: inBar % 4 != 0 ? _prof.AccentOff\n\t\t\t\t: (inBar / 4) % 2 == 1 ? _prof.AccentBack : 1f;\n\t\t\tsk.Accent[c] = Math.Min( 1f, (haveKit ? sk.Accent[c] : 1f) * metric * 0.6f );\n\t\t}\n\t}\n\n\tvoid ArrangeBand( in Part part, Skeleton sk, Rng rng )\n\t{\n\t\t// \u2500\u2500 the parts \u2500\u2500\n\t\t// EVERY CHORUS AGREES, AND THE CHORUS IS STILL ARRANGED. Those are two different claims and\n\t\t// conflating them is what would make this whole phase a no-op: if a chorus quoted the TABLE\n\t\t// rather than quoting the other choruses, the song's own rhythm section would stay one entry\n\t\t// out of a table of three, which is the ceiling this exists to break \u2014 and the choruses are\n\t\t// most of what a listener hears as the song.\n\t\t//\n\t\t// So the chorus is arranged ONCE and cached as the song's own part. Every later chorus\n\t\t// reuses the cached line rather than re-deriving it: the guarantee becomes structural\n\t\t// instead of resting on the skeleton happening to come out the same at three different\n\t\t// points in the song.\n\t\tif ( part.Type == Section.Chorus )\n\t\t{\n\t\t\tif ( !_chorusArranged )\n\t\t\t{\n\t\t\t\t_songBass = Arrange( _songBass, sk, rng, _prof.BassRole, _prof.BassPatterns );\n\t\t\t\t_songComp = Arrange( _songComp, sk, rng, _prof.CompRole, _prof.CompFigures );\n\t\t\t\t// The LOUD figure is the chorus part in any genre that changes technique when the\n\t\t\t\t// section is loud, so leaving it un-arranged would leave exactly the bars a listener\n\t\t\t\t// remembers coming straight out of a table of two.\n\t\t\t\tif ( _songLoud != null )\n\t\t\t\t\t_songLoud = Arrange( _songLoud, sk, rng, _prof.LoudCompRole ?? _prof.CompRole,\n\t\t\t\t\t\t_prof.LoudCompFigures );\n\t\t\t\tif ( _songKeys != null )\n\t\t\t\t\t_songKeys = Arrange( _songKeys, sk, rng, _prof.KeysRole, _prof.KeysFigures );\n\t\t\t\t_chorusArranged = true;\n\t\t\t}\n\t\t\telse { MarkTaken( sk, _songBass ); MarkTaken( sk, _songComp ); MarkTaken( sk, _songKeys ); }\n\t\t\t_bassPat = _songBass; _compFig = _songComp; _keysFig = _songKeys;\n\t\t\treturn;\n\t\t}\n\n\t\t_bassPat = Arrange( _bassPat, sk, rng, _prof.BassRole, _prof.BassPatterns );\n\t\t_compFig = Arrange( _compFig, sk, rng, _prof.CompRole, _prof.CompFigures );\n\t\tif ( _keysFig != null )\n\t\t\t_keysFig = Arrange( _keysFig, sk, rng, _prof.KeysRole, _prof.KeysFigures );\n\t}\n\n\t/// <summary>Whether the song's chorus parts have been arranged yet. The chorus is arranged the\n\t/// first time one is rendered and every later chorus reuses that line.</summary>\n\tbool _chorusArranged, _chorusKitArranged;\n\n\t/// <summary>Whether the kit's patterns for THIS section are final yet \u2014 i.e. whether the accent\n\t/// grid may be built off them.</summary>\n\tbool _kitArranged;\n\n\t/// <summary>\n\t/// The kit, arranged. The drums were the one layer left out of the arranger, and the reasoning\n\t/// was good \u2014 the grooves are fitted to a played corpus, so they were the measured reference the\n\t/// band wrote against rather than another client of it. The cost was that the groove was drawn\n\t/// ONCE PER SONG and never re-drawn: every bar of every section played the identical kick, snare\n\t/// and cymbal, two or three states per genre and exactly one inside a song.\n\t///\n\t/// THE CYMBAL IS NOT ARRANGED. It is the pulse, and it is where the corpus pass found the\n\t/// largest mismatch of all (country's hat on the \"and\", 84% against 36% on the beat) \u2014 leaving\n\t/// it alone preserves that by construction rather than by a rule that can be got wrong. What\n\t/// varies about the cymbal is which INSTRUMENT plays it and how sparse a section thins it, both\n\t/// of which already vary per section.\n\t///\n\t/// EVERY CHORUS AGREES, the same way the band's does and for the same reason: the song's kit is\n\t/// arranged the first time a chorus is rendered and every later chorus replays that line.\n\t/// </summary>\n\tvoid ArrangeKit( Skeleton sk, string bk )\n\t{\n\t\tvar rng = new Rng( $\"{_tag}:kit:{bk}\" );\n\t\tif ( _sectionType == Section.Chorus )\n\t\t{\n\t\t\tif ( !_chorusKitArranged )\n\t\t\t{\n\t\t\t\t_songKick = ArrangeDrum( _songKick, sk, rng, KickRole(), kick: true );\n\t\t\t\t_songSnare = ArrangeDrum( _songSnare, sk, rng, _prof.SnareRole, kick: false );\n\t\t\t\t_chorusKitArranged = true;\n\t\t\t}\n\t\t\t_kickFig = _songKick; _snareFig = _songSnare;\n\t\t\t_kitArranged = true;\n\t\t\treturn;\n\t\t}\n\n\t\t_kickFig = ArrangeDrum( _kickFig, sk, rng, KickRole(), kick: true );\n\t\t_snareFig = ArrangeDrum( _snareFig, sk, rng, _prof.SnareRole, kick: false );\n\t\t_kitArranged = true;\n\t}\n\n\t/// <summary>\n\t/// Where a section is quiet enough that the kit plays the SPINE and nothing else \u2014 the\n\t/// downbeat kick and the struck backbeat, with every ghost and every pushed kick gone.\n\t///\n\t/// AND THE FEEL GATE IS THE POINT, not a caveat. Half time is a pattern RATE: it already\n\t/// stretches the groove to half its density, so a breakdown that also thinned to the spine\n\t/// would be two hits a bar and a hole in the arrangement. Every Breakdown in every form here\n\t/// is <c>feel: 0.5f</c>, which is exactly why this fires on the INTRO instead \u2014 a kit walking\n\t/// in on the bare bones of its own groove, which is what an intro is.\n\t/// </summary>\n\tconst float KitSpineFrom = 0.32f;\n\n\t/// <summary>\n\t/// THE KIT'S DENSITY BIAS, \u22121 (thin) to +1 (fill), and what makes energy an input to what the\n\t/// drummer PLAYS rather than only to how loud it is.\n\t///\n\t/// It shifts the weight between the DROP and ADD mutations, so a quiet section is likelier to\n\t/// lose an onset and a loud one to gain one. Everything a drummer does with dynamics beyond\n\t/// hitting harder is here: fewer notes, dropped ghosts, a busier bar under a chorus.\n\t///\n\t/// <c>DrumBusy</c> and <c>DrumTone</c> feed the same decision rather than sitting on top of it\n\t/// as multipliers \u2014 that is the whole reason they are here. A bright kit is snare-led, so\n\t/// <c>DrumTone</c> pushes the ghost layer up and the foot down; a dark one does the reverse.\n\t/// </summary>\n\tfloat KitBias( bool kick )\n\t{\n\t\tfloat energy = (_energy - 0.5f) * 1.4f;\n\t\tfloat busy = (Math.Clamp( _c.DrumBusy, 0f, 1f ) - 0.5f) * 1.2f;\n\t\tfloat tone = (_drumTone - 0.5f) * 0.6f * (kick ? -1f : 1f);\n\t\treturn Math.Clamp( energy + busy + tone, -1f, 1f );\n\t}\n\n\t/// <summary>The spine alone \u2014 see <see cref=\"KitSpineFrom\"/>.</summary>\n\tstatic Pattern ToSpine( Pattern fig, bool[] spine )\n\t{\n\t\tint n = 0;\n\t\tfor ( int i = 0; i < spine.Length; i++ ) if ( spine[i] ) n++;\n\t\tif ( n == 0 || n == fig.Count ) return fig;\n\t\tvar ticks = new int[n]; var values = new int[n]; var vels = new float[n];\n\t\tfor ( int i = 0, k = 0; i < fig.Count; i++ )\n\t\t{\n\t\t\tif ( !spine[i] ) continue;\n\t\t\tticks[k] = fig.TickAt( i ); values[k] = fig.ValueAt( i ); vels[k] = fig.VelAt( i ); k++;\n\t\t}\n\t\treturn new Pattern( fig.LengthTicks, ticks, values, vels );\n\t}\n\n\t/// <summary>\n\t/// The kick's role, WITH THE SIGN OF ITS COMPLEMENT DECIDED BY WHO WROTE FIRST \u2014 and that sign\n\t/// is the whole difference between the two orderings.\n\t///\n\t/// <see cref=\"Score\"/> reads <c>Complement</c> as a push AWAY from cells the tune and the parts\n\t/// placed so far have taken, which is right for every melodic voice and right for a kick the\n\t/// band has not been written against yet: a leading kit states the beat and the band answers it.\n\t/// A FOLLOWING kick is the opposite gesture. The riff is already on the grid, and a drummer\n\t/// playing to it lands WITH it \u2014 that is what \"the kick tracks the topline\" means in programmed\n\t/// pop and what a riff-led metal foot is doing under a gallop.\n\t///\n\t/// Ordering with the same sign on both sides is what the split reporting caught: it changed who\n\t/// saw whom and left both modes agreeing to within a point or two, which is a mechanism that\n\t/// costs a draw and buys nothing.\n\t/// </summary>\n\tArrangeRole KickRole()\n\t{\n\t\tvar r = _prof.KickRole;\n\t\treturn _kitLeads ? r\n\t\t\t: new ArrangeRole( r.Cells, r.Kick, -r.Complement, r.Seam, r.AddValue );\n\t}\n\n\t/// <summary>One drum, through the same mutations as everything else \u2014 at the kit's own lower\n\t/// rate, with the genre's spine held back, and without writing itself into the occupancy the\n\t/// band reads.\n\t///\n\t/// The RECOMBINE table is this drum's line from the genre's OTHER grooves, which is the same\n\t/// claim the melodic version makes: the genre's own vocabulary, re-cut, and never a gesture the\n\t/// genre does not have.</summary>\n\tPattern ArrangeDrum( Pattern fig, Skeleton sk, Rng rng, in ArrangeRole role, bool kick )\n\t{\n\t\tif ( fig == null ) return null;\n\t\tvar spine = DrumGroove.SpineOf( fig, kick, _time.BarTicks );\n\t\tvar table = new Pattern[_prof.Grooves.Length];\n\t\tfor ( int i = 0; i < table.Length; i++ )\n\t\t\ttable[i] = kick ? _prof.Grooves[i].Kick : _prof.Grooves[i].Snare;\n\t\t// The arrangement happens either way, so the stream costs the same whatever the section's\n\t\t// energy \u2014 the spine section then keeps only what it was always going to keep.\n\t\tvar arranged = Arrange( fig, sk, rng, role, table, _prof.KitMutateRate, spine,\n\t\t\tmarks: false, bias: KitBias( kick ), offPulse: kick );\n\t\tif ( _energy > KitSpineFrom || _feel < 1f ) return arranged;\n\t\treturn ToSpine( arranged, DrumGroove.SpineOf( arranged, kick, _time.BarTicks ) );\n\t}\n\n\t/// <summary>Write a figure's onsets into the skeleton's occupancy without changing it \u2014 what a\n\t/// quoted part still owes the parts arranged after it.</summary>\n\tvoid MarkTaken( Skeleton sk, Pattern fig )\n\t{\n\t\tif ( fig == null ) return;\n\t\tforeach ( var h in fig.Slice( sk.StartTick, sk.StartTick + _sectionTicks, sk.StartTick, _feel ) )\n\t\t{\n\t\t\tint c = sk.CellAt( h.Tick );\n\t\t\tif ( c >= 0 ) sk.Taken[c] = true;\n\t\t}\n\t}\n\n\t/// <summary>How often a non-chorus section plays its figure verbatim rather than working on\n\t/// it. The rest of the weight is shared over the four mutations below.</summary>\n\tconst float QuoteWeight = 1f;\n\n\t/// <summary>\n\t/// Arrange one part: the genre's authored figure, worked on against the section's skeleton.\n\t///\n\t/// THE TABLES ARE SEED MATERIAL, NOT A CEILING. The authored figures are each genre's\n\t/// characteristic gestures and none of them is deleted \u2014 what changes is that a section's part\n\t/// is now figure x mutation x skeleton rather than one entry out of a table of three. That\n\t/// product is where the state count comes from: the whole rhythm section used to have twelve\n\t/// states in punk over five hundred songs, because it was the product of three table sizes and\n\t/// randomness cannot reach past a table size.\n\t///\n\t/// Every mutation stays inside the genre's allowed cell class, so a skank stays offbeat and a\n\t/// punk downstroke stays on the beat however loud the accent grid is elsewhere.\n\t/// </summary>\n\tPattern Arrange( Pattern fig, Skeleton sk, Rng rng, in ArrangeRole role, Pattern[] table )\n\t\t=> Arrange( fig, sk, rng, role, table, _prof.MutateRate );\n\n\t/// <param name=\"spine\">Onsets the mutations may not reach, index-aligned with\n\t/// <paramref name=\"fig\"/> \u2014 the drums' <see cref=\"DrumGroove.SpineOf\"/>. Null for a voice whose\n\t/// whole figure is fair game, which is every melodic one.</param>\n\t/// <param name=\"marks\">Whether the result goes into the skeleton's occupancy. The kit's does\n\t/// not: the kick has its own layer on the grid and the snare is most of the accent grid, so\n\t/// writing it into <c>Taken</c> as well would have the band pushing away from a beat it is\n\t/// supposed to be locking to, and count it twice while doing it.</param>\n\t/// <param name=\"bias\">\u22121 (thin) to +1 (fill): shifts weight between DROP and ADD without\n\t/// changing how much of the stream the draw costs. Zero for every melodic voice, so their\n\t/// weights are exactly what they always were; the kit reads its section's energy and the\n\t/// vibe's DRUM BUSY through it (see <see cref=\"KitBias\"/>).</param>\n\tPattern Arrange( Pattern fig, Skeleton sk, Rng rng, in ArrangeRole role, Pattern[] table,\n\t\tfloat mutateRate, bool[] spine = null, bool marks = true, float bias = 0f,\n\t\tbool offPulse = false )\n\t{\n\t\tif ( fig == null || fig.Count == 0 ) { return fig; }\n\n\t\t// The draw is taken whatever the outcome, so a genre's mutation rate cannot change how much\n\t\t// of this stream the next voice sees \u2014 the same discipline PickOrNull keeps in the composer.\n\t\tfloat mutate = Math.Clamp( mutateRate, 0f, 1f );\n\t\tint op = rng.WeightedIndex( new[]\n\t\t{\n\t\t\t(int)MathF.Round( QuoteWeight * (1f - mutate) * 100f ),   // quote\n\t\t\t(int)MathF.Round( mutate * 30f * (1f - bias) ),           // drop\n\t\t\t(int)MathF.Round( mutate * 30f * (1f + bias) ),           // add\n\t\t\t(int)MathF.Round( mutate * 25f ),                         // displace\n\t\t\t(int)MathF.Round( mutate * 15f ),                         // recombine\n\t\t} );\n\n\t\tvar ticks = new List<int>();\n\t\tvar values = new List<int>();\n\t\tvar vels = new List<float>();\n\t\tfor ( int i = 0; i < fig.Count; i++ )\n\t\t{ ticks.Add( fig.TickAt( i ) ); values.Add( fig.ValueAt( i ) ); vels.Add( fig.VelAt( i ) ); }\n\n\t\tswitch ( op )\n\t\t{\n\t\t\tcase 1: Drop( ticks, values, vels, fig, sk, rng, role, spine ); break;\n\t\t\tcase 2: Add( ticks, values, vels, fig, sk, rng, role, offPulse ); break;\n\t\t\tcase 3: Displace( ticks, values, vels, fig, sk, rng, role, spine, offPulse ); break;\n\t\t\tcase 4: Recombine( ticks, values, vels, fig, rng, table, spine ); break;\n\t\t}\n\n\t\tvar arranged = ticks.Count == 0 ? fig\n\t\t\t: new Pattern( fig.LengthTicks, ticks.ToArray(), values.ToArray(), vels.ToArray() );\n\t\tif ( marks ) MarkTaken( sk, arranged );\n\t\treturn arranged;\n\t}\n\n\t// \u2500\u2500 scoring \u2500\u2500\n\t// A figure loops inside the section, so a candidate position is judged over EVERY repetition it\n\t// will actually be played at rather than over the first one. A one-bar figure in an eight-bar\n\t// section is played eight times; scoring it against bar 1 alone would arrange it for a bar it\n\t// spends seven eighths of its life away from.\n\tfloat Score( int figTick, Pattern fig, Skeleton sk, in ArrangeRole role )\n\t{\n\t\tfloat sum = 0; int n = 0;\n\t\tfor ( int rep = 0; ; rep++ )\n\t\t{\n\t\t\tint t = sk.StartTick + (int)Math.Round( (rep * (double)fig.LengthTicks + figTick) / Math.Max( 0.01f, _feel ) );\n\t\t\tint c = sk.CellAt( t );\n\t\t\tif ( c < 0 ) break;\n\t\t\tsum += sk.Accent[c]\n\t\t\t\t+ role.Kick * (sk.Kick[c] ? 1f : 0f)\n\t\t\t\t+ role.Seam * (sk.Seam[c] ? 1f : 0f)\n\t\t\t\t- role.Complement * ((sk.TuneOn[c] ? 1f : 0f) + (sk.Taken[c] ? 0.6f : 0f));\n\t\t\tn++;\n\t\t}\n\t\treturn n == 0 ? float.NegativeInfinity : sum / n;\n\t}\n\n\tbool AllowedFigTick( int figTick, Pattern fig, Skeleton sk, in ArrangeRole role )\n\t{\n\t\tint c = sk.CellAt( sk.StartTick + (int)Math.Round( figTick / Math.Max( 0.01f, _feel ) ) );\n\t\treturn c >= 0 && figTick % Skeleton.CellTicks == 0 && sk.Allows( role.Cells, c );\n\t}\n\n\t/// <summary>DROP the onset that fights hardest with what is already there \u2014 a tune landing the\n\t/// comp is stepping on, most often. Never the figure's first onset: a figure that loses its\n\t/// downbeat is a different figure.</summary>\n\tvoid Drop( List<int> ticks, List<int> values, List<float> vels, Pattern fig, Skeleton sk,\n\t\tRng rng, in ArrangeRole role, bool[] spine = null )\n\t{\n\t\tif ( ticks.Count <= 2 ) return;\n\t\tint worst = -1; float worstScore = float.MaxValue;\n\t\tfor ( int i = 1; i < ticks.Count; i++ )\n\t\t{\n\t\t\tif ( spine != null && spine[i] ) continue;\n\t\t\tfloat s = Score( ticks[i], fig, sk, role );\n\t\t\tif ( s < worstScore ) { worstScore = s; worst = i; }\n\t\t}\n\t\tif ( worst < 0 ) return;\n\t\tticks.RemoveAt( worst ); values.RemoveAt( worst ); vels.RemoveAt( worst );\n\t}\n\n\t/// <summary>ADD an onset on the best free cell of the genre's allowed class. The new hit takes\n\t/// its VALUE from the onset before it, so it is the same gesture played once more rather than a\n\t/// cell type the figure never used.</summary>\n\t/// <param name=\"offPulse\">Refuse cells on the beat \u2014 the other half of the kick's spine law\n\t/// (see <see cref=\"DrumGroove.SpineOf\"/>). A groove's identity is partly where it does NOT\n\t/// play, and a rule about existing onsets cannot say that: the one drop IS the hole on beat 1,\n\t/// and without this it quietly acquires the downbeat it is defined by not having.</param>\n\tvoid Add( List<int> ticks, List<int> values, List<float> vels, Pattern fig, Skeleton sk,\n\t\tRng rng, in ArrangeRole role, bool offPulse = false )\n\t{\n\t\tint best = -1; float bestScore = float.NegativeInfinity;\n\t\tfor ( int t = 0; t < fig.LengthTicks; t += Skeleton.CellTicks )\n\t\t{\n\t\t\tif ( offPulse && DrumGroove.IsPulse( t ) ) continue;\n\t\t\tif ( ticks.Contains( t ) || !AllowedFigTick( t, fig, sk, role ) ) continue;\n\t\t\tfloat s = Score( t, fig, sk, role );\n\t\t\tif ( s > bestScore ) { bestScore = s; best = t; }\n\t\t}\n\t\tif ( best < 0 ) return;\n\t\tint at = 0;\n\t\twhile ( at < ticks.Count && ticks[at] < best ) at++;\n\t\tint from = Math.Max( 0, at - 1 );\n\t\tticks.Insert( at, best );\n\t\tvalues.Insert( at, role.AddValue == ArrangeRole.NoValue ? values[from] : role.AddValue );\n\t\tvels.Insert( at, vels[from] * 0.9f );\n\t}\n\n\t/// <summary>DISPLACE one onset by a cell, staying inside the allowed class \u2014 the same figure\n\t/// with one hit pushed or pulled. Not the first onset, for the same reason DROP spares it.\n\t/// </summary>\n\t/// <param name=\"offPulse\">As <see cref=\"Add\"/>'s: a kick may not be moved ONTO a beat either.\n\t/// The same hole a groove is defined by is just as fillable by a displaced push as by an added\n\t/// one \u2014 ska's beat 1 still sat 3 points over its baseline once Add alone was stopped. So the\n\t/// kick's on-beat set is frozen entirely: nothing enters it, nothing leaves it, and what\n\t/// arranges is the pushes around it.</param>\n\tvoid Displace( List<int> ticks, List<int> values, List<float> vels, Pattern fig, Skeleton sk,\n\t\tRng rng, in ArrangeRole role, bool[] spine = null, bool offPulse = false )\n\t{\n\t\tif ( ticks.Count <= 1 ) return;\n\t\tint i = 1 + rng.Int( ticks.Count - 1 );\n\t\tint step = rng.Chance( 0.5f ) ? Skeleton.CellTicks : -Skeleton.CellTicks;\n\t\t// Both draws are taken before the spine is consulted, so a groove whose onsets are mostly\n\t\t// spine costs this stream exactly what one whose onsets are all free costs.\n\t\tif ( spine != null && spine[i] ) return;\n\t\t// The allowed class is often coarser than a sixteenth, so widen the move rather than giving\n\t\t// up: a skank displaced by one cell can never be legal, displaced by two it is.\n\t\tfor ( int k = 1; k <= 4; k++ )\n\t\t{\n\t\t\tint t = ticks[i] + step * k;\n\t\t\tif ( t <= 0 || t >= fig.LengthTicks || ticks.Contains( t ) ) continue;\n\t\t\tif ( offPulse && DrumGroove.IsPulse( t ) ) continue;\n\t\t\tif ( !AllowedFigTick( t, fig, sk, role ) ) continue;\n\t\t\t// The VALUE and the VELOCITY move with the tick. A displace that moved only the tick\n\t\t\t// would keep the figure's cells and lose which hit was which \u2014 the chop that got pushed\n\t\t\t// would arrive wearing the next chop's articulation.\n\t\t\tint v = values[i]; float g = vels[i];\n\t\t\tticks.RemoveAt( i ); values.RemoveAt( i ); vels.RemoveAt( i );\n\t\t\tint at = 0;\n\t\t\twhile ( at < ticks.Count && ticks[at] < t ) at++;\n\t\t\tticks.Insert( at, t ); values.Insert( at, v ); vels.Insert( at, g );\n\t\t\treturn;\n\t\t}\n\t}\n\n\t/// <summary>RECOMBINE: take one bar of the phrase from another figure in the same genre's\n\t/// table. The genre's own vocabulary, re-cut \u2014 which is why this is the mutation that reaches\n\t/// furthest without ever producing a gesture the genre does not have.</summary>\n\t/// <param name=\"spine\">Kept where the bar is cleared. Recombine is the one mutation that\n\t/// removes onsets it never looked at \u2014 it replaces a whole bar \u2014 so without this a genre's\n\t/// backbeat would survive Drop and Displace and then vanish anyway one time in six.</param>\n\tvoid Recombine( List<int> ticks, List<int> values, List<float> vels, Pattern fig, Rng rng,\n\t\tPattern[] table, bool[] spine = null )\n\t{\n\t\tif ( table == null || table.Length < 2 ) return;\n\t\tPattern other = null;\n\t\tfor ( int tries = 0; tries < 4 && other == null; tries++ )\n\t\t{\n\t\t\tvar p = table[rng.Int( table.Length )];\n\t\t\tif ( !ReferenceEquals( p, fig ) ) other = p;\n\t\t}\n\t\tif ( other == null ) return;\n\n\t\tint barTicks = _time.BarTicks;\n\t\tint bars = Math.Max( 1, fig.LengthTicks / barTicks );\n\t\tint bar = rng.Int( bars );\n\t\tint from = bar * barTicks, to = from + barTicks;\n\n\t\tfor ( int i = ticks.Count - 1; i >= 0; i-- )\n\t\t\tif ( ticks[i] >= from && ticks[i] < to && (spine == null || !spine[i]) )\n\t\t\t{ ticks.RemoveAt( i ); values.RemoveAt( i ); vels.RemoveAt( i ); }\n\n\t\t// ONE bar of the other figure, folded onto this bar \u2014 and taken from ONE of its bars, not\n\t\t// every bar of it collapsed together. `tick % barTicks` maps a two-bar figure's second bar\n\t\t// back onto its first, so reading the whole thing would interleave two bars' onsets into\n\t\t// one and hand back a list that no longer ascends.\n\t\tint otherBar = (rng.Int( Math.Max( 1, other.LengthTicks / barTicks ) )) * barTicks;\n\t\tfor ( int i = 0; i < other.Count; i++ )\n\t\t{\n\t\t\tint ot = other.TickAt( i );\n\t\t\tif ( ot < otherBar || ot >= otherBar + barTicks ) continue;\n\t\t\tint t = ot - otherBar + from;\n\t\t\tif ( t < from || t >= to || ticks.Contains( t ) ) continue;\n\t\t\tint at = 0;\n\t\t\twhile ( at < ticks.Count && ticks[at] < t ) at++;\n\t\t\tticks.Insert( at, t ); values.Insert( at, other.ValueAt( i ) ); vels.Insert( at, other.VelAt( i ) );\n\t\t}\n\t}\n}\n"
        },
        {
            "Ident": "gamah.skafinity",
            "Path": "Engine/CompFigure.cs",
            "FileName": "CompFigure.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 341414,
            "IsPrivate": false,
            "Code": "using System;\n\nnamespace Skafinity;\n\n/// <summary>\n/// The comp figures \u2014 what rhythm each genre's chordal voices actually play.\n///\n/// This was the loudest remaining duplication in the engine: one <c>KeysOnsets {0,3,4,7}</c>\n/// served rock, country and pop, and one rhythm-guitar loop served rock, country and punk off a\n/// single <c>country</c> bool. Three genres at a time played the SAME comping rhythm, whatever\n/// harmony sat underneath \u2014 and the comp is most of what a listener hears as \"the band\".\n///\n/// A figure is a <see cref=\"Pattern\"/>, so it owns its length: the ska horn answer is two bars\n/// because it IS a call and response, the rock riff is two bars because a riff is a motif rather\n/// than a bar, and punk is one bar because that is the whole idea of punk.\n///\n/// CELL VALUES say how the hit is played; <see cref=\"CompStyle\"/> says what the voice does with\n/// that. <see cref=\"Tone\"/> cells name a single chord tone by index (for arpeggios and\n/// alternating figures) instead of the whole voicing.\n/// </summary>\nstatic class CompFigure\n{\n\t/// <summary>Full voicing, allowed to ring into the next hit.</summary>\n\tpublic const int Ring = 0;\n\t/// <summary>Full voicing, short \u2014 a stab or a chop.</summary>\n\tpublic const int Stab = 1;\n\t/// <summary>Root only, muted \u2014 the palm-muted chug between accents.</summary>\n\tpublic const int Mute = 2;\n\t/// <summary>A single chord tone: <c>Tone(0)</c> is the root, <c>Tone(1)</c> the next voice up.\n\t/// Wraps over the voicing, so an arpeggio can just count upward.</summary>\n\tpublic static int Tone( int i ) => 10 + i;\n\tpublic static bool IsTone( int v ) => v >= 10;\n\tpublic static int ToneIndex( int v ) => v - 10;\n\n\tconst int R = Harmony.Rest;\n\n\tstatic Pattern E( params int[] cells ) => Pattern.Eighths( cells );\n\tstatic Pattern S( params int[] cells ) => Pattern.Sixteenths( cells );\n\tstatic Pattern T( params int[] cells ) => Pattern.ThirtySeconds( cells );\n\n\t// \u2500\u2500 Ska-punk: the skank chop on the offbeats. The second figure answers itself across two bars\n\t// (the push on the \"and of 4\" pulls into bar 2), which one-bar tables could not express.\n\tpublic static readonly Pattern[] SkaPunk =\n\t{\n\t\tE( R, Stab, R, Stab, R, Stab, R, Stab ),\n\t\tE( R, Stab, R, Stab, R, Stab, R, Stab,\n\t\t   R, Stab, R, Stab, R, Stab, Stab, Stab ),\n\t\tE( R, Stab, R, Stab, R, Stab, R, R,\n\t\t   R, Stab, R, Stab, R, Stab, R, Stab ),\n\t};\n\n\t// The flick: the last chop of the two-bar phrase doubles at the THIRTY-SECOND, so the\n\t// skank pushes into the next bar instead of just stopping. Two notes off a wrist that is\n\t// already moving \u2014 the same hand that plays the chop plays the flick.\n\tpublic static readonly Pattern SkaPunkFlick =\n\t\tT( R,R,R,R, Stab,R,R,R, R,R,R,R, Stab,R,R,R,\n\t\t   R,R,R,R, Stab,R,R,R, R,R,R,R, Stab,R,R,R,\n\t\t   R,R,R,R, Stab,R,R,R, R,R,R,R, Stab,R,R,R,\n\t\t   R,R,R,R, Stab,R,R,R, R,R,R,R, Stab,Stab,R,R );\n\n\t// \u2500\u2500 Ska-punk (loud): what the same voice plays once the section is loud \u2014 see GenreProfile.LoudComp.\n\t// The skank stops. These are the guitar part of a third-wave chorus: on the beat, ringing, and\n\t// through a driven amp (so RhythmGtrTone's drive takes the third out and they land as power\n\t// chords). The offbeat does not vanish from the song \u2014 the horns and the kit still carry it,\n\t// which is what keeps a loud ska chorus from simply being a punk chorus.\n\tpublic static readonly Pattern[] SkaPunkLoud =\n\t{\n\t\tE( Ring, R, Stab, R, Ring, R, Stab, R ),\n\t\t// Two bars, the second pushing back into the first: the chorus figure that answers itself.\n\t\tE( Ring, R, R, Stab, Ring, R, Stab, R,\n\t\t   Ring, R, R, Stab, Ring, Stab, Stab, R ),\n\t\t// The one that keeps the offbeat inside the loud part \u2014 downbeat power chord, offbeat\n\t\t// answer. This is the figure that still sounds like ska with the gain on.\n\t\tE( Ring, Stab, R, Stab, Ring, Stab, R, Stab ),\n\t};\n\n\t// \u2500\u2500 Rock: a real two-bar riff motif. NOT an every-eighth chug \u2014 the hits are placed, they\n\t// ring, and the second bar answers the first.\n\tpublic static readonly Pattern[] Rock =\n\t{\n\t\tE( Ring, R, R, Stab, Ring, R, Stab, R,\n\t\t   Ring, R, R, Stab, Ring, R, Stab, Stab ),\n\t\tE( Ring, R, Stab, R, R, Ring, R, Stab,\n\t\t   Ring, R, Stab, R, R, Ring, Stab, R ),\n\t\tE( Ring, Mute, Mute, Ring, R, Mute, Ring, R,\n\t\t   Ring, Mute, Mute, Ring, R, Ring, R, Stab ),\n\t};\n\n\t// The first figure with a THIRTY-SECOND pickup pushing the phrase back round to bar 1 \u2014 a\n\t// riff's pickup is the oldest ornament in rock guitar. Note it REPLACES the last stab\n\t// rather than joining it: an ornament that adds notes makes the comp louder as well as\n\t// busier, and the comp is the bed (see the mix balance in the engine suite).\n\tpublic static readonly Pattern RockPickup =\n\t\tT( Ring,R,R,R, R,R,R,R, R,R,R,R, Stab,R,R,R,\n\t\t   Ring,R,R,R, R,R,R,R, Stab,R,R,R, R,R,R,R,\n\t\t   Ring,R,R,R, R,R,R,R, R,R,R,R, Stab,R,R,R,\n\t\t   Ring,R,R,R, R,R,R,R, Stab,R,R,R, R,R,Stab,Stab );\n\n\t// \u2500\u2500 Rock keys: the syncopated Charleston push. Kept as its own voice with its own figure so\n\t// the two interlock rather than doubling.\n\tpublic static readonly Pattern[] RockKeys =\n\t{\n\t\tE( Ring, R, R, Stab, Ring, R, R, Stab ),\n\t\tE( Ring, R, R, Stab, R, Ring, R, R,\n\t\t   R, Stab, Ring, R, R, Stab, R, R ),\n\t};\n\n\t// \u2500\u2500 Country: the \"chick\" \u2014 a clean strum on every offbeat, over the bass's \"boom\". This is\n\t// the half of boom-chick the guitar owns; the bass tables own the other half.\n\tpublic static readonly Pattern[] Country =\n\t{\n\t\tE( R, Stab, R, Stab, R, Stab, R, Stab ),\n\t\tE( R, Stab, R, Stab, R, Stab, R, Stab,\n\t\t   R, Stab, R, Stab, R, Stab, Stab, R ),\n\t};\n\n\t// Chicken-pickin': a chick snaps a THIRTY-SECOND pull-off after it \u2014 one plucked note and\n\t// one that costs the picking hand nothing, which is why the gesture survives at country's\n\t// fastest and its slowest alike. TWICE in two bars, not on every chick: a second note on\n\t// every one of them is a different instrument rather than an ornament, which is the same\n\t// lesson the lead's double-stops learned.\n\tpublic static readonly Pattern CountryPickOff =\n\t\tT( R,R,R,R, Stab,R,R,R, R,R,R,R, Stab,R,R,R,\n\t\t   R,R,R,R, Stab,R,R,R, R,R,R,R, Stab,R,R,R,\n\t\t   R,R,R,R, Stab,R,R,R, R,R,R,R, Stab,Stab,R,R,\n\t\t   R,R,R,R, Stab,R,R,R, R,R,R,R, Stab,Stab,R,R );\n\n\t// \u2500\u2500 Country keys: honky-tonk piano stabs on 2 and 4 \u2014 it answers the backbeat rather than\n\t// keeping time, which is what stops it from doubling the guitar.\n\tpublic static readonly Pattern[] CountryKeys =\n\t{\n\t\tE( R, R, Stab, R, R, R, Stab, R ),\n\t\tE( R, R, Stab, R, R, R, Stab, Stab,\n\t\t   R, R, Stab, R, Stab, R, Stab, R ),\n\t};\n\n\t// \u2500\u2500 Punk: downstroke eighths, one chord per bar, nothing else. The variation is that the\n\t// four-bar phrase drops a hit at the end to breathe before the turnaround.\n\tpublic static readonly Pattern[] Punk =\n\t{\n\t\tE( Ring, Ring, Ring, Ring, Ring, Ring, Ring, Ring ),\n\t\tE( Ring, Ring, Ring, Ring, Ring, Ring, Ring, Ring,\n\t\t   Ring, Ring, Ring, Ring, Ring, Ring, Ring, Ring,\n\t\t   Ring, Ring, Ring, Ring, Ring, Ring, Ring, Ring,\n\t\t   Ring, Ring, Ring, Ring, Ring, Ring, R, Ring ),\n\t};\n\n\t// The turnaround flurry: two bars of downstrokes, and the last eighth breaks into\n\t// THIRTY-SECONDS on the way back round. Punk's whole idea is that nothing lets up, so the\n\t// ornament is where it hands over, not scattered through the bar.\n\tpublic static readonly Pattern PunkTurnaround =\n\t\tT( Ring,R,R,R, Ring,R,R,R, Ring,R,R,R, Ring,R,R,R,\n\t\t   Ring,R,R,R, Ring,R,R,R, Ring,R,R,R, Ring,R,R,R,\n\t\t   Ring,R,R,R, Ring,R,R,R, Ring,R,R,R, Ring,R,R,R,\n\t\t   Ring,R,R,R, Ring,R,R,R, Ring,R,R,R, Ring,Ring,Ring,Ring );\n\n\t// \u2500\u2500 Metal: the palm-muted gallop, authored at the sixteenth it actually lives on. Ring hits\n\t// are the power-chord accents; everything between them is the muted root.\n\tpublic static readonly Pattern[] Metal =\n\t{\n\t\tS( Ring, Mute, Mute, Mute, Ring, Mute, Mute, Mute,\n\t\t   Ring, Mute, Mute, Mute, Ring, Mute, Mute, Mute ),\n\t\tS( Ring, Mute, Mute, Ring, Mute, Mute, Ring, Mute,\n\t\t   Mute, Ring, Mute, Mute, Ring, Mute, Mute, Mute,\n\t\t   Ring, Mute, Mute, Ring, Mute, Mute, Ring, Mute,\n\t\t   Mute, Ring, Mute, Mute, Ring, Ring, Mute, Mute ),   // the classic \"gallop\" (2 bars)\n\t\tS( Ring, R, Mute, Mute, Ring, R, Mute, Mute,\n\t\t   Ring, R, Mute, Mute, Ring, Ring, R, R ),\n\t};\n\n\t// Authored at the THIRTY-SECOND so the chug can burst. Most of the bar is the same\n\t// sixteenth chug as the figures above (a cell, then a rest cell); the last beat runs\n\t// 32nds into the bar line. That is the tremolo gesture as a player uses it \u2014 a flurry\n\t// pulling into the next downbeat \u2014 rather than a bar of it, and being a handful of notes\n\t// it stays a gesture at the top of the genre's band as much as the bottom.\n\tpublic static readonly Pattern MetalTremolo =\n\t\tT( Ring, R, Mute, R, Mute, R, Mute, R,\n\t\t   Ring, R, Mute, R, Mute, R, Mute, R,\n\t\t   Ring, R, Mute, R, Mute, R, Mute, R,\n\t\t   Ring, R, Mute, R, Mute, Mute, Mute, Mute );\n\n\t// \u2500\u2500 Pop: a held pad. One hit a bar, ringing the whole way \u2014 the harmony is a bed here, not a\n\t// rhythm part, which is exactly what the arp on top needs.\n\tpublic static readonly Pattern[] Pop =\n\t{\n\t\tE( Ring, R, R, R, R, R, R, R ),\n\t\tE( Ring, R, R, R, R, R, R, R,\n\t\t   Ring, R, R, R, R, R, Ring, R ),\n\t};\n\n\t// \u2500\u2500 Pop arp: sixteenths climbing the voicing. The tone indices wrap over whatever voicing\n\t// the song drew, so an add9 arp reaches the 9th without the figure knowing what a 9th is.\n\tpublic static readonly Pattern[] PopArp =\n\t{\n\t\tS( Tone( 0 ), Tone( 1 ), Tone( 2 ), Tone( 3 ), Tone( 2 ), Tone( 1 ), Tone( 2 ), Tone( 3 ),\n\t\t   Tone( 0 ), Tone( 1 ), Tone( 2 ), Tone( 3 ), Tone( 2 ), Tone( 1 ), Tone( 2 ), Tone( 1 ) ),\n\t\tS( Tone( 0 ), R, Tone( 2 ), Tone( 1 ), Tone( 0 ), R, Tone( 2 ), Tone( 3 ),\n\t\t   Tone( 0 ), R, Tone( 2 ), Tone( 1 ), Tone( 3 ), Tone( 2 ), Tone( 1 ), Tone( 0 ) ),\n\t};\n\n\t// The sixteenth arp with a THIRTY-SECOND run home over the last beat \u2014 the synth-pop\n\t// flourish that resolves the two-bar phrase. A sequencer plays it and so does a keyboard\n\t// player; either way it is one beat of the figure, not the figure's rate.\n\tpublic static readonly Pattern PopArpRun =\n\t\tT( Tone(0),R, Tone(1),R, Tone(2),R, Tone(3),R,\n\t\t   Tone(2),R, Tone(1),R, Tone(2),R, Tone(3),R,\n\t\t   Tone(0),R, Tone(1),R, Tone(2),R, Tone(3),R,\n\t\t   Tone(2),R, Tone(1),R, Tone(3),Tone(2), Tone(1),Tone(0) );\n\n\t// \u2500\u2500 Hemiola: the cadential regrouping. Three eighths long, so it does NOT divide the bar \u2014\n\t// the figure and the bar line pull apart and re-converge, which is Biamonte's grouping\n\t// dissonance and the reason Pattern carries its own length at all. Any chordal voice can\n\t// swap to this for the last bars of a section (see MusicGen.RenderSection).\n\tpublic static readonly Pattern Hemiola = E( Stab, R, Stab );\n}\n"
        },
        {
            "Ident": "gamah.skafinity",
            "Path": "Engine/VibeCodec.cs",
            "FileName": "VibeCodec.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 341414,
            "IsPrivate": false,
            "Code": "using System;\nusing System.Collections.Generic;\nusing System.Text;\n\nnamespace Skafinity;\n\n/// <summary>\n/// The knob grid, and its compact hex encoding \u2014 the \"vibe\" half of a seed\n/// (<c>tag:n[:genre][:vibe]</c>; the string as a whole is <see cref=\"SeedCodec\"/>'s).\n///\n/// WIRE FORMAT \u2014 one GLOBAL grid, genre-independent and fixed width:\n///   <c>[voice 0 cols 1..4][voice 1 cols 1..4]\u2026</c>, one hex digit per cell,\n///   <see cref=\"VoiceCount\"/> \u00d7 <see cref=\"WireColumns\"/> = <see cref=\"VibeLength\"/> chars.\n///\n/// An instrument sits at the SAME index in every genre, whether or not that genre plays it, and\n/// every cell is a fixed (Config field, range) pair \u2014 <see cref=\"Cells\"/> \u2014 that no genre may\n/// redefine. That is what makes a vibe portable: pin one, let the genre roll, and each song reads\n/// the same 36 numbers through whatever voices it happens to use. A genre chooses which cells it\n/// EXPOSES as sliders and what to call them (<see cref=\"GenreDef\"/>), and nothing else.\n///\n/// The wire carries a NORMALISED level (0..15 over the cell's range), not a raw value, so a\n/// genre's character comes from its voice code and <see cref=\"GenreProfile\"/> \u2014 the places that\n/// already hold it \u2014 rather than from a per-genre range on the knob.\n///\n/// A vibe is EXACTLY <see cref=\"VibeLength\"/> hex chars. Short, long or non-hex is not a vibe;\n/// there is no pad-with-defaults degrade, because a half-read grid is a song nobody chose. Growing\n/// the grid (a voice, a 5th column) changes that length and invalidates every shared vibe: it is a\n/// format break, not an append. Volume never travels \u2014 it is column 0, a local mix preference.\n///\n/// Lossy by design (16 levels/cell) but stable: Encode(Apply(s)) == s for any valid s.\n/// </summary>\npublic static class VibeCodec\n{\n\tinternal const string Hex = \"0123456789abcdef\";\n\tpublic const int Levels = 16;       // one hex digit per knob\n\tpublic const int Columns = 5;       // 0 volume, then four travelling columns\n\t/// <summary>First column that travels. Column 0 is VOLUME \u2014 a local mix preference\n\t/// (see <see cref=\"ReadVolumes\"/>), so the whole column is skipped rather than encoded.</summary>\n\tpublic const int WireFirstColumn = 1;\n\tpublic const int WireColumns = Columns - WireFirstColumn;\n\n\tpublic sealed class Field\n\t{\n\t\tpublic string Name;\n\t\tpublic float Min, Max;\n\t\tpublic bool Int;\n\t\t/// <summary>Discrete option labels (value = Min + index); null for a continuous knob.</summary>\n\t\tpublic string[] Choices;\n\t\tpublic Func<MusicGen.Config, float> Get;\n\t\tpublic Action<MusicGen.Config, float> Set;\n\t\t/// <summary>Instrument row this knob belongs to.</summary>\n\t\tpublic string Voice;\n\t\t/// <summary>Matrix column: 0 volume, 1..4 the travelling columns.</summary>\n\t\tpublic int Column;\n\n\t\t/// <summary>Current value as a 0..1 fraction of the range.</summary>\n\t\tpublic float GetNorm( MusicGen.Config c ) =>\n\t\t\tMath.Clamp( (Get( c ) - Min) / (Max - Min), 0f, 1f );\n\n\t\t/// <summary>Set from a 0..1 fraction (rounded for integer/discrete knobs).</summary>\n\t\tpublic void SetNorm( MusicGen.Config c, float norm )\n\t\t{\n\t\t\tfloat v = Min + Math.Clamp( norm, 0f, 1f ) * (Max - Min);\n\t\t\tif ( Int || Choices != null ) v = (float)Math.Round( v );\n\t\t\tSet( c, v );\n\t\t}\n\n\t\t/// <summary>Human-readable current value for the row header.</summary>\n\t\tpublic string Display( MusicGen.Config c )\n\t\t{\n\t\t\tfloat v = Get( c );\n\t\t\tif ( Choices != null )\n\t\t\t{\n\t\t\t\tint idx = (int)Math.Clamp( Math.Round( v - Min ), 0, Choices.Length - 1 );\n\t\t\t\treturn Choices[idx];\n\t\t\t}\n\t\t\tif ( Int ) return ((int)Math.Round( v )).ToString();\n\t\t\t// A knob whose whole range fits in 0..2 is a proportion, not a count \u2014 rounding it to\n\t\t\t// a whole number shows the same \"1\" across most of its travel. Read those as percents\n\t\t\t// (a 0..1.5 volume, a 0.7..1.45 tempo scale); anything wider is a real quantity (Hz,\n\t\t\t// cents, a drive amount) and stays a number.\n\t\t\tif ( Max <= 2f ) return $\"{(int)Math.Round( v * 100 )}%\";\n\t\t\treturn ((int)Math.Round( v )).ToString();\n\t\t}\n\t}\n\n\tstatic Field F( string name, float min, float max, bool isInt,\n\t\tFunc<MusicGen.Config, float> get, Action<MusicGen.Config, float> set,\n\t\tstring voice, int column, string[] choices = null )\n\t\t=> new() { Name = name, Min = min, Max = max, Int = isInt, Get = get, Set = set,\n\t\t\tVoice = voice, Column = column, Choices = choices };\n\n\t// \u2500\u2500 The global voice table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\t// Identity is the VOICE, not the label a genre puts on it: ska's \"LEAD\" and pop's \"LEAD\" are\n\t// two different voices (MELODY and LEAD GTR), and pop's \"SYNTH\" is rock's KEYS. Index order is\n\t// the wire order and is fixed; a genre's display order is its own (see GenreDef.Rows).\n\tpublic const int VoiceMelody = 4;\n\n\tsealed class VoiceDef\n\t{\n\t\tpublic string Name;\n\t\tpublic Field Volume;        // column 0 \u2014 never on the wire\n\t\tpublic Field[] Cells;       // columns 1..4, null where the grid has no knob at all\n\t}\n\n\tstatic VoiceDef V( string name, Func<MusicGen.Config, float> volGet, Action<MusicGen.Config, float> volSet,\n\t\tField c1, Field c2, Field c3, Field c4 )\n\t\t=> new() { Name = name, Volume = F( \"VOLUME\", 0f, 1.5f, false, volGet, volSet, name, 0 ),\n\t\t\tCells = new[] { c1, c2, c3, c4 } };\n\n\t// The cell table. A cell's Config field and RANGE are global \u2014 the same 16 levels mean the same\n\t// thing in every genre, which is what a portable vibe requires. Where a genre wants a different\n\t// floor or a different amount of an effect, that already lives in its voice code (Guitar.cs and\n\t// Lead.cs offset the drive per genre) or in GenreProfile; it must not come back here as a\n\t// per-genre range, or a pinned vibe stops meaning one thing.\n\tstatic readonly VoiceDef[] Voices =\n\t{\n\t\tV( \"DRUMS\", c => c.DrumVol, ( c, v ) => c.DrumVol = v,\n\t\t\tF( \"TONE\", 0f, 1f, false, c => c.DrumTone, ( c, v ) => c.DrumTone = v, \"DRUMS\", 1 ),\n\t\t\tF( \"BUSY\", 0f, 1f, false, c => c.DrumBusy, ( c, v ) => c.DrumBusy = v, \"DRUMS\", 2 ),\n\t\t\tF( \"DRIVE\", 0f, 1f, false, c => c.DrumDrive, ( c, v ) => c.DrumDrive = v, \"DRUMS\", 3 ),\n\t\t\tnull ),\n\t\tV( \"BASS\", c => c.BassVol, ( c, v ) => c.BassVol = v,\n\t\t\tF( \"TONE\", 80f, 1200f, false, c => c.BassCutoff, ( c, v ) => c.BassCutoff = v, \"BASS\", 1 ),\n\t\t\tF( \"DRIVE\", 1f, 4f, false, c => c.BassDrive, ( c, v ) => c.BassDrive = v, \"BASS\", 2 ),\n\t\t\tF( \"OCTAVE POP\", 0f, 1f, false, c => c.OctavePopChance, ( c, v ) => c.OctavePopChance = v, \"BASS\", 3 ),\n\t\t\tF( \"TRIPLETS\", 0f, 0.1f, false, c => c.BassTriplets, ( c, v ) => c.BassTriplets = v, \"BASS\", 4 ) ),\n\t\tV( \"SKANK\", c => c.SkankVol, ( c, v ) => c.SkankVol = v,\n\t\t\tF( \"TONE\", 500f, 8000f, false, c => c.SkankCutoff, ( c, v ) => c.SkankCutoff = v, \"SKANK\", 1 ),\n\t\t\tF( \"BITE\", 0f, 2000f, false, c => c.SkankHighpass, ( c, v ) => c.SkankHighpass = v, \"SKANK\", 2 ),\n\t\t\tF( \"CHOP\", 0.15f, 1f, false, c => c.SkankChop, ( c, v ) => c.SkankChop = v, \"SKANK\", 3 ),\n\t\t\tnull ),\n\t\tV( \"ORGAN\", c => c.OrganVol, ( c, v ) => c.OrganVol = v,\n\t\t\tF( \"TONE\", 500f, 8000f, false, c => c.OrganCutoff, ( c, v ) => c.OrganCutoff = v, \"ORGAN\", 1 ),\n\t\t\tF( \"BUBBLE\", 0f, 1f, false, c => c.OrganBubbleChance, ( c, v ) => c.OrganBubbleChance = v, \"ORGAN\", 2 ),\n\t\t\tF( \"VIBRATO\", 0f, 12f, false, c => c.OrganVibrato, ( c, v ) => c.OrganVibrato = v, \"ORGAN\", 3 ),\n\t\t\tnull ),\n\t\tV( \"MELODY\", c => c.MelodyVol, ( c, v ) => c.MelodyVol = v,\n\t\t\tF( \"TONE\", 500f, 8000f, false, c => c.LeadCutoff, ( c, v ) => c.LeadCutoff = v, \"MELODY\", 1 ),\n\t\t\tF( \"JUMPINESS\", 0f, 1f, false, c => c.MelodyLeapChance, ( c, v ) => c.MelodyLeapChance = v, \"MELODY\", 2 ),\n\t\t\tF( \"TRIPLETS\", 0f, 0.1f, false, c => c.TripletChance, ( c, v ) => c.TripletChance = v, \"MELODY\", 3 ),\n\t\t\tnull ),\n\t\tV( \"HORNS\", c => c.HornVol, ( c, v ) => c.HornVol = v,\n\t\t\tF( \"TONE\", 500f, 8000f, false, c => c.HornCutoff, ( c, v ) => c.HornCutoff = v, \"HORNS\", 1 ),\n\t\t\tF( \"SECTION\", 0f, 1f, false, c => c.HornSectionChance, ( c, v ) => c.HornSectionChance = v, \"HORNS\", 2 ),\n\t\t\tF( \"DENSITY\", 0f, 1f, false, c => c.HornDensity, ( c, v ) => c.HornDensity = v, \"HORNS\", 3 ),\n\t\t\tnull ),\n\t\tV( \"KEYS\", c => c.KeysVol, ( c, v ) => c.KeysVol = v,\n\t\t\tF( \"TONE\", 500f, 8000f, false, c => c.KeysCutoff, ( c, v ) => c.KeysCutoff = v, \"KEYS\", 1 ),\n\t\t\tF( \"DISTORTION\", 1f, 5f, false, c => c.KeysDrive, ( c, v ) => c.KeysDrive = v, \"KEYS\", 2 ),\n\t\t\tF( \"CHUG\", 0f, 1f, false, c => c.KeysChug, ( c, v ) => c.KeysChug = v, \"KEYS\", 3 ),\n\t\t\tnull ),\n\t\tV( \"RHYTHM GTR\", c => c.RhythmGtrVol, ( c, v ) => c.RhythmGtrVol = v,\n\t\t\tF( \"TONE\", 500f, 8000f, false, c => c.RhythmGtrCutoff, ( c, v ) => c.RhythmGtrCutoff = v, \"RHYTHM GTR\", 1 ),\n\t\t\tF( \"DISTORTION\", 1f, 6f, false, c => c.RhythmGtrDrive, ( c, v ) => c.RhythmGtrDrive = v, \"RHYTHM GTR\", 2 ),\n\t\t\tF( \"CHUG\", 0f, 1f, false, c => c.RhythmGtrChug, ( c, v ) => c.RhythmGtrChug = v, \"RHYTHM GTR\", 3 ),\n\t\t\tnull ),\n\t\tV( \"LEAD GTR\", c => c.LeadGtrVol, ( c, v ) => c.LeadGtrVol = v,\n\t\t\tF( \"TONE\", 500f, 8000f, false, c => c.LeadGtrCutoff, ( c, v ) => c.LeadGtrCutoff = v, \"LEAD GTR\", 1 ),\n\t\t\tF( \"DISTORTION\", 1f, 6f, false, c => c.LeadGtrDrive, ( c, v ) => c.LeadGtrDrive = v, \"LEAD GTR\", 2 ),\n\t\t\tF( \"BENDINESS\", 0f, 1f, false, c => c.LeadGtrBend, ( c, v ) => c.LeadGtrBend = v, \"LEAD GTR\", 3 ),\n\t\t\tnull ),\n\t};\n\n\tpublic static int VoiceCount => Voices.Length;\n\t/// <summary>Exact length of a vibe string, derived from the grid so nothing restates it.</summary>\n\tpublic static readonly int VibeLength = Voices.Length * WireColumns;\n\n\t/// <summary>The wire position of voice <paramref name=\"v\"/>'s column <paramref name=\"col\"/>.</summary>\n\tstatic int Pos( int v, int col ) => v * WireColumns + (col - WireFirstColumn);\n\n\t/// <summary>Is there a knob at wire position <paramref name=\"pos\"/>? The grid is rectangular, so\n\t/// some cells are holes \u2014 a voice with three columns still reserves its fourth. A hole encodes\n\t/// as '0' and decodes to nothing, and a roll must leave it at '0' too: anything else would\n\t/// re-encode to '0' and make a rolled vibe fail to round-trip.</summary>\n\tpublic static bool HasCell( int pos )\n\t{\n\t\tif ( pos < 0 || pos >= VibeLength ) return false;\n\t\treturn Voices[pos / WireColumns].Cells[pos % WireColumns] != null;\n\t}\n\n\t// \u2500\u2500 Advanced / tuning-only knobs \u2500\u2500\n\t// Config fields that shape the BASELINE MIX (peak balances, kit presence) rather than a\n\t// song's shareable identity. They are NOT in the vibe wire (Encode/Apply never touch them)\n\t// and NOT in Fields() (so they don't appear as per-genre sliders). Membership in THIS list\n\t// is exactly the \"config value, not a vibe slider\" marker. Surfaced to the host (web:\n\t// config.json) by NAME \u2014 names match the MusicGen.Config field 1:1 \u2014 so the house mix can\n\t// be retuned at runtime without a rebuild. Ranges are generous tuning bounds, not the seed\n\t// grid. Genre-independent; not positional, so nothing here can shift a wire cell.\n\tpublic static readonly Field[] AdvancedFields =\n\t{\n\t\tF( \"KitPresence\", 0f, 4f, false, c => c.KitPresence, ( c, v ) => c.KitPresence = v, null, 0 ),\n\t\t// The stereo image, and the house's scale over each song's drawn reverb. Both were vibe\n\t\t// sliders; both are environment rather than music. 1 = as designed.\n\t\tF( \"PanAmount\", 0f, 1f, false, c => c.PanAmount, ( c, v ) => c.PanAmount = v, null, 0 ),\n\t\tF( \"MasterReverb\", 0f, 2f, false, c => c.MasterReverb, ( c, v ) => c.MasterReverb = v, null, 0 ),\n\t\t// How far each genre's own mix profile (GenreProfile.Mix) is taken. 1 = as designed,\n\t\t// 0 = every genre through one neutral mix. The SHAPE of a genre's mix is character and\n\t\t// lives in the profile; what the house retunes at runtime is how far to push it.\n\t\tF( \"GenreMix\", 0f, 2f, false, c => c.GenreMix, ( c, v ) => c.GenreMix = v, null, 0 ),\n\t\tF( \"KickBalance\", 0f, 2f, false, c => c.KickBalance, ( c, v ) => c.KickBalance = v, null, 0 ),\n\t\tF( \"SnareBalance\", 0f, 2f, false, c => c.SnareBalance, ( c, v ) => c.SnareBalance = v, null, 0 ),\n\t\tF( \"TomBalance\", 0f, 2f, false, c => c.TomBalance, ( c, v ) => c.TomBalance = v, null, 0 ),\n\t\tF( \"HatBalance\", 0f, 2f, false, c => c.HatBalance, ( c, v ) => c.HatBalance = v, null, 0 ),\n\t\tF( \"RideBalance\", 0f, 2f, false, c => c.RideBalance, ( c, v ) => c.RideBalance = v, null, 0 ),\n\t\tF( \"CrashBalance\", 0f, 2f, false, c => c.CrashBalance, ( c, v ) => c.CrashBalance = v, null, 0 ),\n\t\tF( \"BassBalance\", 0f, 2f, false, c => c.BassBalance, ( c, v ) => c.BassBalance = v, null, 0 ),\n\t\tF( \"SkankBalance\", 0f, 2f, false, c => c.SkankBalance, ( c, v ) => c.SkankBalance = v, null, 0 ),\n\t\tF( \"OrganBalance\", 0f, 2f, false, c => c.OrganBalance, ( c, v ) => c.OrganBalance = v, null, 0 ),\n\t\tF( \"MelodyBalance\", 0f, 2f, false, c => c.MelodyBalance, ( c, v ) => c.MelodyBalance = v, null, 0 ),\n\t\tF( \"HornBalance\", 0f, 2f, false, c => c.HornBalance, ( c, v ) => c.HornBalance = v, null, 0 ),\n\t\tF( \"KeysBalance\", 0f, 2f, false, c => c.KeysBalance, ( c, v ) => c.KeysBalance = v, null, 0 ),\n\t\tF( \"RhythmGtrBalance\", 0f, 2f, false, c => c.RhythmGtrBalance, ( c, v ) => c.RhythmGtrBalance = v, null, 0 ),\n\t\tF( \"LeadGtrBalance\", 0f, 2f, false, c => c.LeadGtrBalance, ( c, v ) => c.LeadGtrBalance = v, null, 0 ),\n\t\t// Stereo double-tracking / width (see MusicGen.Config \"width\" block).\n\t\tF( \"DoubleTrack\", 0f, 1f, false, c => c.DoubleTrack, ( c, v ) => c.DoubleTrack = v, null, 0 ),\n\t\tF( \"WidthBacking\", 0f, 1f, false, c => c.WidthBacking, ( c, v ) => c.WidthBacking = v, null, 0 ),\n\t\tF( \"WidthLead\", 0f, 1f, false, c => c.WidthLead, ( c, v ) => c.WidthLead = v, null, 0 ),\n\t\t// Bounded at 20 cents, not 50: half a quarter-tone between two takes is not a double, it is\n\t\t// a tuning error, and this is a house-config field with no way for a listener to undo it.\n\t\tF( \"WidthDetune\", 0f, 20f, false, c => c.WidthDetune, ( c, v ) => c.WidthDetune = v, null, 0 ),\n\t\tF( \"WidthDelayMs\", 0f, 40f, false, c => c.WidthDelayMs, ( c, v ) => c.WidthDelayMs = v, null, 0 ),\n\t\tF( \"WidthJitterMs\", 0f, 30f, false, c => c.WidthJitterMs, ( c, v ) => c.WidthJitterMs = v, null, 0 ),\n\t\tF( \"WidthAmpVar\", 0f, 1f, false, c => c.WidthAmpVar, ( c, v ) => c.WidthAmpVar = v, null, 0 ),\n\t\tF( \"WidthCutoffVar\", 0f, 1f, false, c => c.WidthCutoffVar, ( c, v ) => c.WidthCutoffVar = v, null, 0 ),\n\t};\n\n\t/// <summary>Overlay a <c>name \u2192 raw value</c> map (the shared config file's \"advanced\" block)\n\t/// onto <paramref name=\"c\"/>. Keys match <see cref=\"AdvancedFields\"/> names (= Config field\n\t/// names) 1:1; unknown keys are ignored and values are clamped to each field's range. Both\n\t/// hosts use this: s&box reads the file and calls this; the web mirrors it in JS over the\n\t/// same field list. Call it where the baseline mix is assembled (after defaults/vibe).</summary>\n\tpublic static void ApplyAdvanced( IReadOnlyDictionary<string, float> values, MusicGen.Config c )\n\t{\n\t\tif ( c == null || values == null ) return;\n\t\tforeach ( var f in AdvancedFields )\n\t\t\tif ( values.TryGetValue( f.Name, out var v ) )\n\t\t\t\tf.Set( c, Math.Clamp( v, f.Min, f.Max ) );\n\t}\n\n\t// \u2500\u2500 Genres: which cells they expose, in what order, under what name \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\t// A genre NEVER redefines what a cell does \u2014 it picks the rows a listener gets sliders for and\n\t// the words on them. Everything a genre sounds like is in its voice code and GenreProfile.\n\tsealed class RowDef\n\t{\n\t\tpublic int Voice;                 // index into Voices\n\t\tpublic string Label;              // what this genre calls it (null = the voice's own name)\n\t\tpublic int[] Columns;             // which travelling columns it exposes\n\t\tpublic string[] Labels;           // per-column name override (null entry = the cell's own)\n\t}\n\n\tsealed class GenreDef\n\t{\n\t\tpublic string Name;\n\t\tpublic RowDef[] Rows;             // display order\n\t}\n\n\t// All four cells of a voice, under the voice's own names. The common case.\n\tstatic RowDef R( int voice, string label = null )\n\t\t=> new() { Voice = voice, Label = label, Columns = null, Labels = null };\n\n\t// A subset of a voice's cells, optionally renamed: R( V, \"SYNTH\", (1, null), (3, \"PLUCK\") ).\n\tstatic RowDef R( int voice, string label, params (int col, string name)[] cells )\n\t{\n\t\tvar cols = new int[cells.Length];\n\t\tvar names = new string[cells.Length];\n\t\tfor ( int i = 0; i < cells.Length; i++ ) { cols[i] = cells[i].col; names[i] = cells[i].name; }\n\t\treturn new RowDef { Voice = voice, Label = label, Columns = cols, Labels = names };\n\t}\n\n\tconst int Drums = 0, Bass = 1, Skank = 2, Organ = 3, Melody = 4, Horns = 5,\n\t\tKeys = 6, RhythmGtr = 7, LeadGtr = 8;\n\n\tstatic readonly GenreDef[] GenreDefs =\n\t{\n\t\t// Ska-Punk. The chorus guitar is the RHYTHM GTR voice: third-wave ska drops the skank for\n\t\t// driven power chords once the section is loud (GenreProfile.LoudComp).\n\t\tnew() { Name = \"Ska-Punk\", Rows = new[]\n\t\t{\n\t\t\tR( Bass ), R( Skank ), R( Organ ), R( Melody, \"LEAD\" ), R( Horns ), R( Drums ),\n\t\t\tR( RhythmGtr, \"CHORUS GTR\" ),\n\t\t} },\n\t\tnew() { Name = \"Rock\", Rows = new[]\n\t\t{\n\t\t\tR( Drums ), R( Bass ), R( Keys ), R( LeadGtr ), R( RhythmGtr ),\n\t\t} },\n\t\t// Country \u2014 clean strummed open chords, honky-tonk piano, twangy telecaster lead. The\n\t\t// cleaner floor under each DISTORTION knob is in Guitar.cs / Lead.cs / Keys.cs.\n\t\tnew() { Name = \"Country\", Rows = new[]\n\t\t{\n\t\t\tR( Drums ), R( Bass ), R( RhythmGtr ), R( Keys ), R( LeadGtr ),\n\t\t} },\n\t\tnew() { Name = \"Metal\", Rows = new[]\n\t\t{\n\t\t\tR( Drums ), R( Bass ), R( RhythmGtr ), R( LeadGtr ),\n\t\t} },\n\t\t// Punk \u2014 \"lean punk\" / power-pop: rock's voices without the keys.\n\t\tnew() { Name = \"Punk\", Rows = new[]\n\t\t{\n\t\t\tR( Drums ), R( Bass ), R( RhythmGtr ), R( LeadGtr ),\n\t\t} },\n\t\t// Pop \u2014 modern synth/dance-pop. The KEYS voice run clean and bright (PLUCK tightens the\n\t\t// ringing pad toward stabs) and the LEAD GTR voice run clean as a plucky synth lead. Both\n\t\t// hide their DISTORTION cell: pop's clean floor is Keys.cs / Lead.cs, and a slider that\n\t\t// the voice code overrules is a lie. The cell still TRAVELS \u2014 every vibe is full width.\n\t\tnew() { Name = \"Pop\", Rows = new[]\n\t\t{\n\t\t\tR( Drums ), R( Bass ),\n\t\t\tR( Keys, \"SYNTH\", (1, null), (3, \"PLUCK\") ),\n\t\t\tR( LeadGtr, \"LEAD\", (1, null), (3, \"GLIDE\") ),\n\t\t} },\n\t};\n\n\tpublic static int GenreCount => GenreDefs.Length;\n\tpublic static IReadOnlyList<string> Genres\n\t{\n\t\tget { var a = new string[GenreDefs.Length]; for ( int i = 0; i < a.Length; i++ ) a[i] = GenreDefs[i].Name; return a; }\n\t}\n\n\tstatic GenreDef Def( int genre ) => GenreDefs[Math.Clamp( genre, 0, GenreDefs.Length - 1 )];\n\n\t/// <summary>A genre's row as the UI shows it: the voice's field, relabelled where the genre\n\t/// says so. The returned Field is a copy \u2014 the global cell is never mutated.</summary>\n\tstatic Field Labelled( Field cell, string voiceLabel, string nameOverride )\n\t{\n\t\tif ( cell == null ) return null;\n\t\tif ( voiceLabel == null && nameOverride == null ) return cell;\n\t\treturn new Field\n\t\t{\n\t\t\tName = nameOverride ?? cell.Name, Min = cell.Min, Max = cell.Max, Int = cell.Int,\n\t\t\tChoices = cell.Choices, Get = cell.Get, Set = cell.Set,\n\t\t\tVoice = voiceLabel ?? cell.Voice, Column = cell.Column,\n\t\t};\n\t}\n\n\t/// <summary>Flat list of the sliders <paramref name=\"genre\"/> shows, in display order: each\n\t/// row's volume then its exposed cells. Each field carries its <see cref=\"Field.Voice\"/> /\n\t/// <see cref=\"Field.Column\"/>, so the UI lays the matrix out without a second table.</summary>\n\t/// <remarks>The returned fields are STABLE: the same genre hands back the same objects every\n\t/// call, because a UI identifies a knob by reference to find its wire index. <see cref=\"Labelled\"/>\n\t/// mints a fresh <see cref=\"Field\"/> for any row its genre renames, so a list rebuilt per call\n\t/// makes those knobs unfindable \u2014 they draw and drag and set nothing, and only on the genres that\n\t/// rename a row. Cached per genre for that reason first and for the allocations second.</remarks>\n\tpublic static IReadOnlyList<Field> Fields( int genre )\n\t{\n\t\tint g = Math.Clamp( genre, 0, GenreDefs.Length - 1 );\n\t\tvar cache = _fields ??= new IReadOnlyList<Field>[GenreDefs.Length];\n\t\tif ( cache[g] != null ) return cache[g];\n\n\t\tvar list = new List<Field>();\n\t\tforeach ( var row in GenreDefs[g].Rows )\n\t\t{\n\t\t\tvar v = Voices[row.Voice];\n\t\t\tlist.Add( Labelled( v.Volume, row.Label, null ) );\n\t\t\tif ( row.Columns == null )\n\t\t\t{\n\t\t\t\tforeach ( var cell in v.Cells )\n\t\t\t\t\tif ( cell != null ) list.Add( Labelled( cell, row.Label, null ) );\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tfor ( int i = 0; i < row.Columns.Length; i++ )\n\t\t\t\t{\n\t\t\t\t\tvar cell = v.Cells[row.Columns[i] - WireFirstColumn];\n\t\t\t\t\tif ( cell != null ) list.Add( Labelled( cell, row.Label, row.Labels[i] ) );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tcache[g] = list;\n\t\treturn list;\n\t}\n\n\t// One list per genre, built on first ask. Lazy rather than a static initialiser so it cannot\n\t// depend on field-initialisation order with GenreDefs.\n\tstatic IReadOnlyList<Field>[] _fields;\n\n\t/// <summary>True if <paramref name=\"f\"/> is a per-instrument VOLUME knob \u2014 column 0 of an\n\t/// instrument row, kept out of the shareable seed and persisted per-voice instead.</summary>\n\tpublic static bool IsVolume( Field f ) => f != null && f.Voice != null && f.Column == 0;\n\n\t/// <summary>Read the per-instrument volumes of <paramref name=\"genre\"/> off\n\t/// <paramref name=\"c\"/> as a <c>voice \u2192 0..1 level</c> map. The key is the label this genre\n\t/// uses, which is what the UI shows; a voice a genre renames (pop's \"SYNTH\") therefore keeps\n\t/// its own level there. Merge this into a single store across genres.</summary>\n\tpublic static Dictionary<string, float> ReadVolumes( int genre, MusicGen.Config c )\n\t{\n\t\tvar d = new Dictionary<string, float>();\n\t\tif ( c == null ) return d;\n\t\tforeach ( var f in Fields( genre ) )\n\t\t\tif ( IsVolume( f ) ) d[f.Voice] = f.GetNorm( c );\n\t\treturn d;\n\t}\n\n\t/// <summary>Overlay a <c>voice \u2192 0..1 level</c> map (from <see cref=\"ReadVolumes\"/> / storage)\n\t/// onto <paramref name=\"c\"/> for <paramref name=\"genre\"/>. Voices absent from the map keep\n\t/// their current/default level. Call this after <see cref=\"Apply\"/> so a song's saved mix\n\t/// rides on top of the seed's voicing.</summary>\n\tpublic static void ApplyVolumes( int genre, IReadOnlyDictionary<string, float> vols, MusicGen.Config c )\n\t{\n\t\tif ( c == null || vols == null ) return;\n\t\tforeach ( var f in Fields( genre ) )\n\t\t\tif ( IsVolume( f ) && vols.TryGetValue( f.Voice, out var n ) )\n\t\t\t\tf.SetNorm( c, n );\n\t}\n\n\t// \u2500\u2500 The wire \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\t/// <summary>Encode the whole global grid off <paramref name=\"c\"/>. Genre-independent: the\n\t/// result depends on the knobs and on nothing else, so re-encoding after a genre change hands\n\t/// back the same string.</summary>\n\tpublic static string Encode( MusicGen.Config c )\n\t{\n\t\tif ( c == null ) return \"\";\n\t\tvar sb = new StringBuilder( VibeLength );\n\t\tforeach ( var v in Voices )\n\t\t\tforeach ( var cell in v.Cells )\n\t\t\t\tsb.Append( cell != null ? Quant( cell, c ) : Hex[0] );\n\t\treturn sb.ToString();\n\t}\n\n\tstatic char Quant( Field f, MusicGen.Config c )\n\t{\n\t\tint q = (int)Math.Round( f.GetNorm( c ) * (Levels - 1) );\n\t\treturn Hex[Math.Clamp( q, 0, Levels - 1 )];\n\t}\n\n\t/// <summary>True if <paramref name=\"s\"/> is a vibe: exactly <see cref=\"VibeLength\"/> hex\n\t/// chars. There is no near-miss \u2014 see the class remarks on why short does not degrade.</summary>\n\tpublic static bool IsVibe( string s )\n\t{\n\t\tif ( s == null || s.Length != VibeLength ) return false;\n\t\tforeach ( var ch in s )\n\t\t\tif ( Hex.IndexOf( char.ToLowerInvariant( ch ) ) < 0 ) return false;\n\t\treturn true;\n\t}\n\n\t/// <summary>Apply a vibe string onto <paramref name=\"c\"/> in place. Returns false (touching\n\t/// nothing) if it is not a vibe \u2014 callers that need to TELL the listener use\n\t/// <see cref=\"SeedCodec.TryParse\"/>, which is where the message lives.</summary>\n\tpublic static bool Apply( string vibe, MusicGen.Config c )\n\t{\n\t\tif ( c == null || !IsVibe( vibe ) ) return false;\n\t\tvibe = vibe.ToLowerInvariant();\n\t\tfor ( int v = 0; v < Voices.Length; v++ )\n\t\t\tforeach ( var cell in Voices[v].Cells )\n\t\t\t{\n\t\t\t\tif ( cell == null ) continue;\n\t\t\t\tint q = Hex.IndexOf( vibe[Pos( v, cell.Column )] );\n\t\t\t\tcell.SetNorm( c, q / (float)(Levels - 1) );\n\t\t\t}\n\t\treturn true;\n\t}\n\n\t// \u2500\u2500 Rolling \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\t/// <summary>\n\t/// Roll a whole vibe STRING \u2014 every cell of the global grid, genre-independent.\n\t///\n\t/// This is the one definition of what \"reroll\" means, shared by every player, so the two\n\t/// drivers cannot answer the question differently. It produces a string rather than editing a\n\t/// Config on purpose: a rolled vibe is full width like any other, so it can be pinned into a\n\t/// seed and heard identically under a genre that was rolled separately.\n\t///\n\t/// Randomness is the CALLER's: <paramref name=\"rnd\"/> returns values in [0,1). A driver that\n\t/// wants a throwaway roll passes a session RNG; one that wants a reproducible roll passes a\n\t/// seeded stream (see <see cref=\"SeedCodec.RollVibeFor\"/>). The engine stays free of any\n\t/// ambient RNG.\n\t/// </summary>\n\tpublic static string RollVibe( Func<float> rnd )\n\t{\n\t\tif ( rnd == null ) return new string( Hex[0], VibeLength );\n\t\tvar sb = new StringBuilder( VibeLength );\n\t\tfor ( int i = 0; i < VibeLength; i++ )\n\t\t{\n\t\t\tif ( !HasCell( i ) ) { sb.Append( Hex[0] ); continue; }   // a hole stays a hole\n\t\t\t// Guard the top of the range: a generator returning exactly 1.0 must not index off the\n\t\t\t// end of the alphabet.\n\t\t\tint q = (int)(rnd() * Levels);\n\t\t\tsb.Append( Hex[Math.Clamp( q, 0, Levels - 1 )] );\n\t\t}\n\t\treturn sb.ToString();\n\t}\n\n\t/// <summary>Roll a genre index from the same kind of caller-owned stream.</summary>\n\tpublic static int RollGenre( Func<float> rnd )\n\t\t=> rnd == null ? 0 : Math.Clamp( (int)(rnd() * GenreCount), 0, GenreCount - 1 );\n\n\t/// <summary>Roll the per-instrument volumes of <paramref name=\"genre\"/> in place \u2014 the one\n\t/// thing a vibe string cannot carry, for a driver that wants the mix rolled too.</summary>\n\tpublic static void RollVolumes( int genre, MusicGen.Config c, Func<float> rnd )\n\t{\n\t\tif ( c == null || rnd == null ) return;\n\t\tforeach ( var f in Fields( genre ) )\n\t\t\tif ( IsVolume( f ) ) f.SetNorm( c, rnd() );\n\t}\n}\n"
        },
        {
            "Ident": "gamah.skafinity",
            "Path": ".obj/__compiler_extra.cs",
            "FileName": "__compiler_extra.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 341414,
            "IsPrivate": false,
            "Code": "global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonTitle\", \"Skafinity\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonIdent\", \"skafinity\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"OrgIdent\", \"gamah\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"Ident\", \"gamah.skafinity\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineVersion\", \"28\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineMinorVersion\", \"1\" )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \".NETCoreApp,Version=v9.0\", FrameworkDisplayName = \".NET 9.0\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"CompileTime\", \"2026-08-13T10:27:48.6244157Z\" )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\"0.0.110.0\")]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\"0.0.110.0\")]"
        },
        {
            "Ident": "gamah.skafinity",
            "Path": "Engine/Melody.cs",
            "FileName": "Melody.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 341414,
            "IsPrivate": false,
            "Code": "using System;\nusing System.Collections.Generic;\n\nnamespace Skafinity;\n\n/// <summary>\n/// The TUNE \u2014 the part of a song a listener could hum back.\n///\n/// Everything the engine generated before this was accompaniment plus an improvisation: the\n/// chordal voices played a rhythm figure, and the lead invented a fresh phrase every two bars.\n/// That is a backing track, not a song. Real rock, punk, ska and pop songs are built on a\n/// MELODY that recurs \u2014 the chorus states the same tune every time it comes round, and that\n/// repetition is what makes it a chorus rather than another eight bars.\n///\n/// A tune is a <see cref=\"Pattern\"/> whose cell values are SCALE DEGREES relative to the key's\n/// tonic (not to the current chord), so the line keeps its shape while the harmony moves under\n/// it \u2014 which is what a melody is. <see cref=\"MusicGen.RenderTune\"/> resolves a degree against\n/// the bar's chord on the strong beats, so the tune stays consonant without being re-written\n/// chord by chord.\n///\n/// Because it is a Pattern it inherits everything patterns get: it anchors to the section (so a\n/// four-bar tune restarts with the chorus) and it stretches under a half-time feel.\n/// </summary>\nstatic class Melody\n{\n\t/// <summary>Cell value for a rest \u2014 no onset, the previous note holds.</summary>\n\tpublic const int Rest = Harmony.Rest;\n\n\t/// <summary>The AMBITUS \u2014 the range a whole tune is written in, in SCALE DEGREES from the key's\n\t/// tonic: from the sixth below it up to the third above the octave. Twelve degrees, about an\n\t/// octave and a fifth in a major scale, and deliberately lopsided \u2014 a melody sits above its\n\t/// tonic and only dips under it, so a symmetric range would spend half of itself where no tune\n\t/// goes.\n\t///\n\t/// It is an authored bound rather than a measured one: what it is FOR is that a line which\n\t/// wanders further stops being singable, and singable is what makes the thing a tune. The\n\t/// number is a judgement about that and nothing more.</summary>\n\tpublic const int DegreeMin = -2, DegreeMax = 9;\n\n\t/// <summary>How many scale degrees ONE PHRASE may cover \u2014 eight, an octave in a major scale,\n\t/// inside the twelve the whole tune may reach.\n\t///\n\t/// A RANGE AND AN AMBITUS ARE TWO DIFFERENT NUMBERS AND ONE CANNOT DO BOTH JOBS. The ambitus is\n\t/// a whole-song figure \u2014 how far the tune goes over all of it \u2014 and a tune here is 2\u20138 bars, so\n\t/// bounding a single phrase with it was measuring one thing and spending it on another. What a\n\t/// phrase actually does is orbit a register: it opens somewhere, moves about an octave around\n\t/// that, and the tune gets its wider reach from the phrases sitting in DIFFERENT places rather\n\t/// than from any one of them wandering.\n\t///\n\t/// So the window is drawn per phrase and anchored on the note the phrase opens on\n\t/// (<see cref=\"Opens\"/>), which is why the opening degree keeps its weighting instead of being\n\t/// folded into a window drawn first. Inside a phrase this is the bound the walk reflects off\n\t/// and the centre <see cref=\"Centre\"/> pulls toward; the ambitus stays the outer wall.\n\t///\n\t/// THE WINDOW BOUNDS WHERE A LINE WANDERS, NOT WHERE IT MAY BE PUT. An answer transposes its\n\t/// call bodily (<see cref=\"AnswerOp.SequenceUp2\"/> takes it up two degrees), and that is a\n\t/// deliberate move rather than a walk drifting out of register \u2014 so <see cref=\"Answer\"/>\n\t/// reflects off the ambitus. Folding a sequence back into the call's window would flatten the\n\t/// one gesture in the tune whose whole point is that it goes somewhere else.\n\t///\n\t/// Authored like everything else here (there is no melodic corpus in this repo). The published\n\t/// pop-melody work that gives whole-song ambitus at around two octaves measures range on a\n\t/// rolling two-bar window for exactly this reason, but its figures are for a different roster\n\t/// and are not borrowed as a number \u2014 this is a judgement about a phrase being one gesture in\n\t/// one register, and <c>--stats</c> reports what the engine actually does with it.</summary>\n\tpublic const int PhraseSpan = 8;\n\n\t/// <summary>The note lengths a tune may be written in, in ticks: sixteenth, eighth, dotted\n\t/// eighth, quarter, dotted quarter, half. <see cref=\"Timing.TicksPerBeat\"/> is 48, so every one\n\t/// of them is exact and <see cref=\"Timing\"/> needs nothing \u2014 the same clean division the\n\t/// thirty-second work already proved.\n\t///\n\t/// The line every genre's weights are split on is the QUARTER: the first three are shorter than\n\t/// a beat and the last three are a beat or longer, which is what <c>move</c> leans on to make a\n\t/// verse sparser than its chorus without a second density mechanism.</summary>\n\tpublic static readonly int[] Lengths = { 12, 24, 36, 48, 72, 96 };\n\n\t/// <summary>Index of the first length that is a beat or longer.</summary>\n\tconst int LongFrom = 3;\n\n\t/// <summary>How a phrase answers itself. The answer used to not be DRAWN at all: it was the\n\t/// call's degrees minus one, every genre, every song, with a forced tonic on the end \u2014 so half\n\t/// of every tune in the engine was a mechanical transform of the other half.</summary>\n\tpublic enum AnswerOp\n\t{\n\t\t/// <summary>The call a step lower \u2014 the old behaviour, and still the heaviest weight in\n\t\t/// most genres because it is genuinely the commonest answer in this music.</summary>\n\t\tTranspose,\n\t\t/// <summary>The same line with a different landing: identical degrees, and the last two\n\t\t/// re-drawn to step home. What a chorus does.</summary>\n\t\tNewTail,\n\t\t/// <summary>The call restated a degree higher \u2014 a question answered with a bigger question,\n\t\t/// which still resolves because the last note is the tonic either way.</summary>\n\t\tSequenceUp,\n\t\t/// <summary>Restated two degrees higher.</summary>\n\t\tSequenceUp2,\n\t\t/// <summary>Mirrored about the call's first degree: where the call rose, the answer falls.\n\t\t/// </summary>\n\t\tInvert,\n\t}\n\n\tpublic static readonly AnswerOp[] Answers =\n\t{\n\t\tAnswerOp.Transpose, AnswerOp.NewTail, AnswerOp.SequenceUp, AnswerOp.SequenceUp2, AnswerOp.Invert,\n\t};\n\n\t/// <summary>How the CONSEQUENT opens \u2014 the one decision that makes a period a period.\n\t///\n\t/// A period is two call/answer pairs: an antecedent that leaves the line open and a consequent\n\t/// that closes it. Both pairs are built by exactly the machinery below; what varies is how much\n\t/// of the antecedent's call the consequent's call keeps. That is the classical taxonomy and it\n\t/// is also the whole variation budget \u2014 the answers repeat their own call's rhythm either way,\n\t/// so if the consequent's call does not move, nothing in the tune's second half is new.</summary>\n\tpublic enum PeriodShape\n\t{\n\t\t/// <summary>PARALLEL \u2014 the consequent restates the call note for note and differs only in\n\t\t/// how it answers. The commonest period in this music, and the one that makes the tune\n\t\t/// unmistakably one tune; it is also the least new material, which is why it is not the\n\t\t/// only shape.</summary>\n\t\tParallel,\n\t\t/// <summary>VARIED \u2014 the consequent keeps the call's rhythm and sings a fresh contour over\n\t\t/// it. The rhythm is what a listener remembers, so this reads as the same phrase said again\n\t\t/// differently rather than as a second idea.</summary>\n\t\tVaried,\n\t\t/// <summary>CONTRASTING \u2014 the consequent opens with a phrase of its own, rhythm and all.\n\t\t/// The departure, and the only shape that puts a second rhythm in the tune.</summary>\n\t\tContrasting,\n\t}\n\n\tpublic static readonly PeriodShape[] Shapes =\n\t{\n\t\tPeriodShape.Parallel, PeriodShape.Varied, PeriodShape.Contrasting,\n\t};\n\n\t/// <summary>Authored, not measured \u2014 there is no melodic corpus in this repo (see the note on\n\t/// <c>GenreProfile.Tune</c>) and this is a judgement about how much a tune may move under\n\t/// itself. It is one table rather than six because nothing found says a genre has an opinion\n\t/// about it; a genre that turns out to want one puts weights in <see cref=\"TuneVocab\"/>, the\n\t/// way <see cref=\"Answers\"/> already does.</summary>\n\tstatic readonly int[] ShapeWeights = { 4, 3, 3 };\n\n\t/// <summary>Where an ANTECEDENT lands: a chord tone that is not the tonic, which is what leaves\n\t/// the line open. The fifth is the half cadence proper and takes most of the weight; the third\n\t/// is the softer one. Landing home here would close the tune half way through it and make the\n\t/// consequent an appendix rather than an answer.</summary>\n\tstatic readonly int[] HalfCadence = { 4, 2 };\n\tstatic readonly int[] HalfCadenceWeights = { 3, 2 };\n\n\t/// <summary>The fewest bars a phrase may be. A period is four phrases, so a tune shorter than\n\t/// four of these is two phrases and no period \u2014 a one-bar \"phrase\" is a fragment, and four of\n\t/// them is a tune that restates itself every bar, which is the defect this exists to fix\n\t/// arriving from the other direction.</summary>\n\tpublic const int MinPhraseBars = 2;\n\n\t/// <summary>How many phrases a tune of <paramref name=\"bars\"/> bars is written in: four (a\n\t/// period) where they are long enough to be phrases, two (a plain call and answer) otherwise.\n\t/// </summary>\n\tpublic static int PhraseCount( int bars ) => bars >= 4 * MinPhraseBars ? 4 : 2;\n\n\t/// <summary>Where a tune may open \u2014 chord tones only, weighted toward the tonic and the fifth,\n\t/// with the octave reachable.</summary>\n\tstatic readonly int[] Opens = { 0, 2, 4, 7 };\n\tstatic readonly int[] OpenWeights = { 5, 3, 4, 2 };\n\n\t/// <summary>How far the phrase leans uphill at its start and downhill at its end \u2014 the MELODIC\n\t/// ARCH. Phrases in this music (and in every corpus anyone has counted) rise and then fall on\n\t/// average, and a plain random walk does not: it wanders, and the only thing that ever brought\n\t/// it home was the forced tonic on the last note, which is a landing with no approach to it.\n\t///\n\t/// 0 would be the old coin toss; 0.5 would make direction deterministic and turn every tune\n\t/// into the same hill. This is a lean on a draw, not a shape imposed on one.</summary>\n\tconst float Arch = 0.25f;\n\n\t/// <summary>How hard the line is pulled back toward the middle of its PHRASE WINDOW \u2014\n\t/// TESSITURA, the fact that a melody orbits a central pitch rather than diffusing across\n\t/// everything it is allowed to sing.\n\t///\n\t/// It is what actually keeps a tune off the range ends. <see cref=\"Reflect\"/> is a BACKSTOP: it\n\t/// stops a line parking at a boundary, but a walk with no centre still spends its time out\n\t/// there, and the arch makes that worse in the first half of every phrase by leaning uphill\n\t/// whatever the register already is. The two are different jobs and both are needed \u2014 this\n\t/// decides where the line lives, reflection decides what happens when it arrives at an edge\n\t/// anyway. The centre it pulls toward is the PHRASE's (<see cref=\"PhraseSpan\"/>), so a phrase\n\t/// orbits its own register rather than the middle of everything the tune may reach.</summary>\n\tconst float Centre = 0.30f;\n\n\t/// <summary>\n\t/// Draw a tune: <paramref name=\"bars\"/> bars built as a PERIOD where they are long enough for\n\t/// one, and as a plain call and answer where they are not.\n\t///\n\t/// A call and answer is a pair of phrases \u2014 the first states a shape and leaves it open, the\n\t/// second repeats that rhythm and resolves it home. That symmetry is most of what makes a line\n\t/// sound composed rather than generated, and a fresh random phrase every two bars never sounds\n\t/// like a tune however good the notes are.\n\t///\n\t/// A PERIOD IS TWO OF THOSE PAIRS AND SITS ABOVE THEM, NOT INSTEAD OF THEM. The antecedent\n\t/// (call, answer) lands on a chord tone that is not the tonic and so leaves the line open; the\n\t/// consequent (call, answer) opens from the antecedent \u2014 restating it, varying it, or departing\n\t/// from it (<see cref=\"PeriodShape\"/>) \u2014 and resolves home. That is what puts repetition at the\n\t/// whole tune's length and variation at a phrase's, instead of the binary shape the tune had\n\t/// before this: two phrases, one rhythm between them, looped to fill the section and repeated\n\t/// identically at every chorus.\n\t///\n\t/// THE RHYTHM REPEAT STAYS, WITHIN A PAIR. Varying an answer's rhythm stops its two phrases\n\t/// being heard as a question and an answer at all; the only rhythmic freedom an answer gets is\n\t/// where its last notes land, and that arrives through <see cref=\"AnswerOp.NewTail\"/> rather\n\t/// than through a second rhythm draw. A new rhythm enters a tune at the CONSEQUENT'S CALL or\n\t/// nowhere. The 100%-tonic ending stays too \u2014 that is not a defect to be varied away, it is\n\t/// what makes the thing a tune.\n\t/// </summary>\n\t/// <param name=\"v\">The genre's vocabulary \u2014 the note lengths it sings in, how often it rests,\n\t/// how often it leaps, and how it answers itself.</param>\n\t/// <param name=\"move\">How much this line moves relative to the genre's own table: 1 for a\n\t/// chorus, less for the sparser verse tune. It leans the length draw toward the long end rather\n\t/// than being a second density knob sitting beside the weights.</param>\n\t/// <param name=\"swung\">True where the song swings or shuffles. THE SIXTEENTH COMES OUT OF THE\n\t/// MENU: under a shuffle the beat's own subdivision IS the triplet, and Timing's warp puts a\n\t/// straight sixteenth at a third of the beat while the band's eighth-based figures sit on the\n\t/// beat and at two thirds. That is not syncopation, it is two grids at once, and it reads as\n\t/// the lead pushing against a band it does not line up with. A shuffled genre's melody moves in\n\t/// eighths and the shuffle does the subdividing.</param>\n\tpublic static Pattern Draw( Rng rng, int bars, int barTicks, in TuneVocab v, float move = 1f,\n\t\tbool swung = false )\n\t{\n\t\tint phrases = PhraseCount( bars );\n\t\tint phraseTicks = barTicks * Math.Max( 1, bars / phrases );\n\t\tvar ticks = new List<int>();\n\t\tvar degrees = new List<int>();\n\n\t\t// The genre's length table, leaned toward the long end for a verse. At move = 1 both\n\t\t// factors are 1 and the table is the genre's verbatim.\n\t\tvar weights = new int[Lengths.Length];\n\t\tfor ( int i = 0; i < weights.Length; i++ )\n\t\t{\n\t\t\tfloat w = v.LengthWeights[i] * (i < LongFrom ? move : 2f - move);\n\t\t\t// Anything that does not divide the eighth: the sixteenth AND the dotted eighth, which\n\t\t\t// lands mid-eighth for the same reason and was the half of this that was easy to miss.\n\t\t\tif ( swung && Lengths[i] % Timing.TicksPerEighth != 0 ) w = 0f;\n\t\t\tweights[i] = Math.Max( 0, (int)MathF.Round( w * 8f ) );\n\t\t}\n\n\t\t// \u2500\u2500 the antecedent \u2500\u2500\n\t\tvar callRhythm = DrawRhythm( rng, phraseTicks, weights, v );\n\t\tvar callDegrees = DrawContour( rng, callRhythm.Count, v );\n\t\tEmit( ticks, degrees, 0, callRhythm, callDegrees );\n\n\t\t// A HALF CADENCE IS WHAT MAKES THE CONSEQUENT NECESSARY. With a period the antecedent lands\n\t\t// on a chord tone that is not the tonic and stays open; with only two phrases there is\n\t\t// nothing after it, so it resolves the way it always did.\n\t\tint open = phrases == 4 ? HalfCadence[rng.WeightedIndex( HalfCadenceWeights )] : 0;\n\t\tEmit( ticks, degrees, phraseTicks, callRhythm, Answer( rng, callDegrees, v, open ) );\n\n\t\tif ( phrases == 4 )\n\t\t{\n\t\t\tvar shape = rng.PickWeighted( Shapes, ShapeWeights );\n\t\t\t// BOTH DRAWN FOR EVERY SHAPE, so swapping one shape for another does not shift the rest\n\t\t\t// of the tune's stream \u2014 the discipline PickOrNull keeps in the composer. A parallel\n\t\t\t// consequent pays for a phrase it does not sing.\n\t\t\tvar freshRhythm = DrawRhythm( rng, phraseTicks, weights, v );\n\t\t\tvar conRhythm = shape == PeriodShape.Contrasting ? freshRhythm : callRhythm;\n\t\t\tvar freshDegrees = DrawContour( rng, conRhythm.Count, v );\n\t\t\tvar conDegrees = shape == PeriodShape.Parallel ? callDegrees : freshDegrees;\n\n\t\t\tEmit( ticks, degrees, 2 * phraseTicks, conRhythm, conDegrees );\n\t\t\t// The consequent answers with its own operator \u2014 that is what a parallel period varies,\n\t\t\t// and it is the only thing it varies.\n\t\t\tEmit( ticks, degrees, 3 * phraseTicks, conRhythm, Answer( rng, conDegrees, v, 0 ) );\n\t\t}\n\n\t\t// A held final note, so the tune breathes before it comes round again.\n\t\treturn new Pattern( bars * barTicks, ticks.ToArray(), degrees.ToArray() );\n\t}\n\n\t/// <summary>Append one phrase's onsets (offset to <paramref name=\"at\"/>) and its degrees.\n\t/// </summary>\n\tstatic void Emit( List<int> ticks, List<int> degrees, int at, List<int> rhythm, List<int> pitches )\n\t{\n\t\tfor ( int i = 0; i < rhythm.Count; i++ ) { ticks.Add( at + rhythm[i] ); degrees.Add( pitches[i] ); }\n\t}\n\n\t/// <summary>One phrase's RHYTHM \u2014 the onsets, in ticks from the phrase's own start.\n\t///\n\t/// Rhythm first, and separately from the pitches: a melody's rhythm is what gets remembered,\n\t/// and drawing it on its own is what lets an answer repeat it exactly.\n\t///\n\t/// A REST IS AN OMITTED ONSET, not a cell. Leaving the tick out means the previous note's\n\t/// SpanTicks simply grows to cover the gap, and RenderTune's two-beat length cap turns the\n\t/// remainder into real silence \u2014 so rests cost the renderer nothing. A Melody.Rest CELL\n\t/// would be read as a DEGREE by RenderTune, which has no rest arm, and sung.</summary>\n\tstatic List<int> DrawRhythm( Rng rng, int phraseTicks, int[] weights, in TuneVocab v )\n\t{\n\t\tvar rhythm = new List<int>();\n\t\tfor ( int t = 0; t < phraseTicks; )\n\t\t{\n\t\t\t// THE PHRASE RE-ANCHORS TO THE BEAT, and without this the widened vocabulary is worse\n\t\t\t// than the two lengths it replaced. Free-running the accumulator over the menu means a\n\t\t\t// dotted eighth or a sixteenth shifts EVERY REMAINING NOTE of the phrase by a non-beat\n\t\t\t// amount, permanently \u2014 the line rotates against the bar and never comes back, which is\n\t\t\t// a 3-against-4 running for eight bars rather than a melody. It reads as the lead being\n\t\t\t// out of time with the band, because it is.\n\t\t\t//\n\t\t\t// The rule is the one a player reads off a stave: inside a beat you may only play what\n\t\t\t// fits the rest of it. So a dotted eighth is followed by a sixteenth, a sixteenth by\n\t\t\t// whatever fills the remaining three, and the next beat starts on the beat. Notes still\n\t\t\t// land on the \"and\" and on sixteenths \u2014 what they cannot do is drift.\n\t\t\tint inBeat = t % Timing.TicksPerBeat;\n\t\t\tint len;\n\t\t\tif ( inBeat == 0 ) len = Lengths[rng.WeightedIndex( weights )];\n\t\t\telse\n\t\t\t{\n\t\t\t\tint room = Timing.TicksPerBeat - inBeat;\n\t\t\t\tvar fits = new int[Lengths.Length];\n\t\t\t\tbool any = false;\n\t\t\t\tfor ( int i = 0; i < Lengths.Length; i++ )\n\t\t\t\t\tif ( Lengths[i] <= room ) { fits[i] = weights[i]; any |= weights[i] > 0; }\n\t\t\t\tlen = any ? Lengths[rng.WeightedIndex( fits )] : room;\n\t\t\t}\n\t\t\t// Never open the phrase on silence: a tune that starts by not being there has no shape\n\t\t\t// for the answer to repeat.\n\t\t\tif ( rhythm.Count == 0 || !rng.Chance( v.Rest ) ) rhythm.Add( t );\n\t\t\tt += len;\n\t\t}\n\t\t// A call of one note is not a call. Only reachable when every cell after the first drew a\n\t\t// rest, which is rare and still worth not shipping.\n\t\tif ( rhythm.Count < 2 ) rhythm.Add( phraseTicks / 2 );\n\t\treturn rhythm;\n\t}\n\n\t/// <summary>One phrase's CONTOUR \u2014 <paramref name=\"notes\"/> degrees relative to the key's tonic.\n\t///\n\t/// Three things shape it and none of them is a random walk: the arch (see <see cref=\"Arch\"/>),\n\t/// post-skip reversal (below), and reflection off the range ends instead of a clamp.\n\t///\n\t/// A phrase opens on a CHORD TONE \u2014 a melody that opens on the second or the seventh is a\n\t/// melody that starts by needing to resolve \u2014 weighted toward the tonic and the fifth, where\n\t/// far more tunes actually start, with the octave reachable. A uniform draw over three values\n\t/// is the sort of thing that shows up in a sweep as 33/33/33 and in a listen as \"they all start\n\t/// the same way\".</summary>\n\tstatic List<int> DrawContour( Rng rng, int notes, in TuneVocab v )\n\t{\n\t\tvar degrees = new List<int>( notes );\n\t\tint degree = Opens[rng.WeightedIndex( OpenWeights )];\n\t\t// THE PHRASE'S OWN WINDOW, drawn around the note the phrase opens on so that the opening\n\t\t// degree keeps its weighting and cannot land outside its own register. Where the window may\n\t\t// sit is what gives the tune its wider reach: two phrases an octave apart cover the ambitus\n\t\t// between them without either of them wandering.\n\t\tint loMin = Math.Max( DegreeMin, degree - (PhraseSpan - 1) );\n\t\tint loMax = Math.Min( degree, DegreeMax - (PhraseSpan - 1) );\n\t\tif ( loMax < loMin ) loMax = loMin;\n\t\tint lo = Math.Min( loMin + rng.Int( loMax - loMin + 1 ), DegreeMax );\n\t\tint hi = Math.Min( lo + PhraseSpan - 1, DegreeMax );\n\t\tint owed = 0;\n\t\tfor ( int i = 0; i < notes; i++ )\n\t\t{\n\t\t\tdegrees.Add( degree );\n\n\t\t\tbool leap = rng.Next() < v.Leap;\n\t\t\t// A leap is a third, a fourth or a fifth. It used to be a third and nothing else, in\n\t\t\t// every genre and every song \u2014 \"a leap\" was one interval wearing a general name.\n\t\t\tint size = leap ? 2 + rng.Int( 3 ) : 1;\n\t\t\tint sign;\n\t\t\tif ( owed != 0 )\n\t\t\t{\n\t\t\t\t// POST-SKIP REVERSAL: a melody that jumps comes back. It is one of the most robust\n\t\t\t\t// findings there is about how tunes are actually written, and it is also what makes\n\t\t\t\t// a leap read as a gesture rather than as the line relocating.\n\t\t\t\tsign = owed;\n\t\t\t\towed = 0;\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tfloat u = notes < 2 ? 0.5f : i / (float)(notes - 1);\n\t\t\t\t// Where in the PHRASE'S window this note sits, \u22121 at the bottom and +1 at the top.\n\t\t\t\tfloat mid = (lo + hi) / 2f, half = Math.Max( 1f, (hi - lo) / 2f );\n\t\t\t\tfloat pos = (degree - mid) / half;\n\t\t\t\tsign = rng.Chance( Math.Clamp( 0.5f + Arch * (1f - 2f * u) - Centre * pos, 0.05f, 0.95f ) )\n\t\t\t\t\t? 1 : -1;\n\t\t\t}\n\t\t\tif ( leap ) owed = -sign;\n\t\t\tdegree = Reflect( degree + sign * size, lo, hi );\n\t\t}\n\t\treturn degrees;\n\t}\n\n\t/// <summary>ANSWER a call: the same rhythm, the call's degrees put through one of the genre's\n\t/// <see cref=\"AnswerOp\"/>s, landing on <paramref name=\"last\"/> \u2014 the tonic where this phrase\n\t/// closes the tune, an open chord tone where it is an antecedent handing over to a consequent.\n\t/// Every operator answers the same question and every one of them lands in the same place.\n\t/// </summary>\n\tstatic List<int> Answer( Rng rng, List<int> call, in TuneVocab v, int last )\n\t{\n\t\tvar op = rng.PickWeighted( Answers, v.AnswerWeights );\n\t\t// Drawn for every operator, so swapping one for another does not shift the rest of the\n\t\t// tune's stream \u2014 the same discipline PickOrNull keeps in the composer.\n\t\tint approach = rng.Chance( 0.65f ) ? 1 : -1;\n\t\tint n = call.Count;\n\t\tint first = call[0];\n\t\tvar answer = new List<int>( n );\n\t\tfor ( int i = 0; i < n; i++ )\n\t\t{\n\t\t\tif ( i == n - 1 ) { answer.Add( Reflect( last ) ); continue; }\n\t\t\tint d = op switch\n\t\t\t{\n\t\t\t\tAnswerOp.NewTail => i == n - 2 ? last + approach : call[i],\n\t\t\t\tAnswerOp.SequenceUp => call[i] + 1,\n\t\t\t\tAnswerOp.SequenceUp2 => call[i] + 2,\n\t\t\t\tAnswerOp.Invert => 2 * first - call[i],\n\t\t\t\t_ => call[i] - 1,\n\t\t\t};\n\t\t\tanswer.Add( Reflect( d ) );\n\t\t}\n\t\treturn answer;\n\t}\n\n\t/// <summary>Fold a degree back inside the singable range by REFLECTING off its ends.\n\t///\n\t/// A clamp is sticky: a line that reaches a boundary and keeps stepping outward parks there,\n\t/// which is where 10\u201318% of every tune's notes sat and where most of its repeated adjacent\n\t/// notes came from \u2014 the two are the same defect seen from either side. Reflection keeps the\n\t/// range (that is what makes a tune singable) and turns the wall into a turn.\n\t///\n\t/// Off the AMBITUS \u2014 the outer wall, which is what a transposed answer folds against.</summary>\n\tinternal static int Reflect( int degree ) => Reflect( degree, DegreeMin, DegreeMax );\n\n\t/// <summary>Fold a degree back inside an arbitrary window by reflecting off its ends \u2014 the\n\t/// walk inside one phrase uses its own (<see cref=\"PhraseSpan\"/>).</summary>\n\tinternal static int Reflect( int degree, int lo, int hi )\n\t{\n\t\tfor ( int guard = 0; guard < 8 && (degree < lo || degree > hi); guard++ )\n\t\t{\n\t\t\tif ( degree < lo ) degree = 2 * lo - degree;\n\t\t\tif ( degree > hi ) degree = 2 * hi - degree;\n\t\t}\n\t\treturn Math.Clamp( degree, lo, hi );\n\t}\n}\n\n// The tune, and how a bar of it is played. Part of the MusicGen engine \u2014 see MusicGen.cs.\n\npublic sealed partial class MusicGen\n{\n\t// The song's tunes, drawn once per song off their own streams (so having them shifts nothing\n\t// else in the composition) and keyed by SECTION TYPE. The chorus tune is the hook: identical\n\t// every chorus, which is the whole reason a chorus reads as one. The verse tune is a second,\n\t// sparser line \u2014 same song, different words.\n\tPattern _chorusTune, _verseTune;\n\n\t// How long one PHRASE of them is. A diagnostic wanting to read a tune phrase by phrase cannot\n\t// re-derive this without re-deciding the period, which is the re-implementation PlanTrace\n\t// exists to avoid \u2014 so the composer writes it down.\n\tint _tunePhraseTicks;\n\n\t/// <summary>One phrase of the song's tunes, in ticks (diagnostics \u2014 see <see cref=\"Melody\"/>).\n\t/// </summary>\n\tinternal int TunePhraseTicks => _tunePhraseTicks;\n\n\t/// <summary>The tune this section sings, or null where the section is not a place for one:\n\t/// a solo is where the genre's lead grammar improvises, an intro is a build-in, and the\n\t/// ending has already resolved.</summary>\n\tPattern TuneFor( Section s ) => !SectionSingsTune( s ) ? null\n\t\t: s == Section.Chorus ? _chorusTune : _verseTune;\n\n\t/// <summary>Whether a section TYPE is a place for a tune at all. Static because it is a\n\t/// property of the form rather than of a drawn song \u2014 which is what lets a form be checked for\n\t/// putting its feel changes somewhere the melody can contrast with them.</summary>\n\tinternal static bool SectionSingsTune( Section s ) =>\n\t\ts is Section.Chorus or Section.Verse or Section.PreChorus or Section.Bridge;\n\n\t/// <summary>Draw the song's tunes \u2014 one for choruses, a sparser one for verses. Every genre\n\t/// gets both: \"riff-led\" does not mean melody-free, and metal verses with no tune left four\n\t/// and eight bar holes where the lead simply did not play.</summary>\n\tvoid DrawTunes( int barTicks, bool swung )\n\t{\n\t\t// The vocabulary is the GENRE's (GenreProfile.Tune). It used to be a switch on _prof.Lead\n\t\t// right here, which is the `if ( _genre == \u2026 )` smell one level removed: two genres sharing\n\t\t// a LeadStyle got the same tune vocabulary, and where their densities matched the draws\n\t\t// agreed and the tunes came back identical.\n\t\t// The tune is a WHOLE NUMBER OF HARMONIC CYCLES \u2014 the bars it takes the progression to come\n\t\t// round (ChordBars x the progression's length), capped at eight. A four-bar tune over an\n\t\t// eight-bar cycle states itself twice, and the second statement lands over different\n\t\t// chords than it was written against: same notes, different harmony, which is exactly the\n\t\t// \"the lead clashes with the backing\" it sounds like. Matching the cycle means every\n\t\t// repetition sits over the changes it was drawn for.\n\t\tint cycle = Math.Clamp( _chordBars * _prog.Length, 2, 8 );\n\t\t// A PERIOD NEEDS FOUR PHRASES, AND A WHOLE NUMBER OF CYCLES IS STILL ALIGNED TO THE CHANGES.\n\t\t// The clamp above is not about length, it is about a tune's statements landing over the\n\t\t// chords they were drawn against \u2014 and a tune of exactly two cycles does, bar for bar. So a\n\t\t// genre whose cycle is short doubles the tune rather than being stuck with two phrases: punk\n\t\t// and pop (ChordBars 1 x a four-chord progression) went from a 4-bar tune whose rhythmic cell\n\t\t// was 2 bars, stated twice and looped, to an 8-bar period. Eight is the ceiling because a\n\t\t// section is eight bars \u2014 a tune longer than the section it is sung in never finishes.\n\t\tint bars = cycle;\n\t\twhile ( bars * 2 <= 8 ) bars *= 2;\n\t\t_tunePhraseTicks = barTicks * bars / Melody.PhraseCount( bars );\n\t\t// THE GENRE IS IN THE TUNE'S STREAM, and it is the only stream it is in.\n\t\t//\n\t\t// Without it the genre reached Draw through nothing but `density` and `leap`, so where two\n\t\t// genres' densities were close the draws mostly agreed and the tunes came back\n\t\t// BYTE-IDENTICAL: over 500 songs, rock and country sang the same melody 53% of the time and\n\t\t// punk and pop 52%. Same n, different key, different kit, literally the same tune \u2014 which is\n\t\t// most of why the roster read as one band.\n\t\t//\n\t\t// The SONG stream (ComposePlan's `new Rng( _tag )`) still has no genre in it, and that is a\n\t\t// feature rather than an oversight: genre 0 and genre 3 at the same tag:n share the root\n\t\t// note, the pan, the ride preference and the whole kit draw, so the same song in two genres\n\t\t// is a thing the toy can do. That is worth more than the variation putting the genre there\n\t\t// would buy, and it is why the genre goes in the TUNE streams and nowhere else.\n\t\t//\n\t\t// IT IS A DIFFERENT DRAW, NOT A GUARANTEED DIFFERENT TUNE, and that distinction is the\n\t\t// point. Two genres landing on a similar melody at one seed is the toy doing what it is\n\t\t// for; what was wrong before was that they landed there RELIABLY, off a stream that could\n\t\t// not tell them apart. Nothing here should ever grow into machinery that forces two genres\n\t\t// to diverge \u2014 the collision rate is something `--stats` reports, not something the engine\n\t\t// enforces.\n\t\t_chorusTune = Melody.Draw( new Rng( $\"{_tag}:tune:{_genre}:chorus\" ), bars, barTicks, _prof.Tune, 1f, swung );\n\t\t// The verse tune is the same vocabulary, sung with fewer notes in it \u2014 same song,\n\t\t// different words.\n\t\t_verseTune = Melody.Draw( new Rng( $\"{_tag}:tune:{_genre}:verse\" ), bars, barTicks, _prof.Tune, 0.8f, swung );\n\t}\n\n\t/// <summary>Play one bar of the section's tune.\n\t///\n\t/// Degrees are relative to the KEY, so the tune keeps its shape as the chords move. What\n\t/// keeps it consonant is resolution on the strong beats only: a note landing on a beat is\n\t/// pulled to the nearest tone of the bar's chord, while the notes between beats are free to\n\t/// pass through. Snapping everything would rewrite the tune chord by chord \u2014 which is\n\t/// exactly the \"no tune, just an improvisation over the changes\" this replaces.</summary>\n\tvoid RenderTune( Pattern tune, int barTick, int barTicks, int chord, Rng rng, Rng exprRng )\n\t{\n\t\tint melBase = LeadBase();\n\t\tvar tones = ChordDegrees( chord );\n\t\tbool guitarLead = !_hornLead;\n\t\tfloat amp = (guitarLead ? _c.LeadGtrVol * _c.LeadGtrBalance : _c.MelodyVol * _c.MelodyBalance)\n\t\t\t* _midMul;\n\t\tfloat drive = guitarLead ? _c.LeadGtrDrive : _c.MelodyDrive;\n\t\tvar ex = guitarLead ? Expr( \"LEAD GTR\" ) : Expr( \"LEAD\" );\n\t\tint prevMidi = NoPrev;\n\n\t\t// A SECTION SHORTER THAN THE TUNE SINGS THE TUNE'S END, not its beginning. A four-bar\n\t\t// pre-chorus over an eight-bar tune stated the call and was cut off by the chorus before\n\t\t// the answer ever arrived \u2014 a phrase interrupted by the next phrase, which is what \"two\n\t\t// ideas at once\" sounds like. Pulling the anchor back lands the tune's resolution exactly\n\t\t// on the section's last bar, which is what a pre-chorus is for.\n\t\tint anchor = _sectionTicks > 0 && _sectionTicks < tune.LengthTicks\n\t\t\t? _sectionTick - (tune.LengthTicks - _sectionTicks)\n\t\t\t: _sectionTick;\n\t\t// THE TUNE IS EXEMPT FROM THE SECTION'S FEEL, and that exemption IS half/double time.\n\t\t// Part.Feel is the RHYTHM SECTION's pattern rate: when a section halves or doubles, the\n\t\t// band changes rate underneath a vocal that stays exactly where it was \u2014 that contrast is\n\t\t// the entire gesture, and it is what makes a double-time chorus lift rather than sound\n\t\t// like the tape sped up. Scaling the hook by the same multiplier deletes the gesture and\n\t\t// leaves only a faster song. So the tune slices at the nominal rate; every other voice\n\t\t// (comp, keys, bass, horns, kit) reads _feel.\n\t\tvar sung = tune.Slice( barTick, barTick + barTicks, anchor );\n\t\tTrace?.Add( TraceVoice.Tune, sung );\n\t\tforeach ( var h in sung )\n\t\t{\n\t\t\tint degree = h.Value;\n\t\t\tint len = Math.Min( h.SpanTicks, Timing.TicksPerBeat * 2 );\n\t\t\tbool onBeat = (h.Tick - _barTick) % Timing.TicksPerBeat == 0;\n\n\t\t\t// What resolves is the note the ear has TIME to hear against the chord: anything on a\n\t\t\t// beat, and anything held for a beat or more. A quick note between beats is a passing\n\t\t\t// tone and is left alone \u2014 that is the difference between a melody and an arpeggio.\n\t\t\t// (Snapping only the on-beat notes left long off-beat non-chord tones ringing over the\n\t\t\t// backing for up to two beats, which is what a clash sounds like.)\n\t\t\tbool resolve = onBeat || len >= Timing.TicksPerBeat;\n\t\t\tif ( resolve ) degree = NearestChordTone( tones, degree );\n\t\t\tint midi = ScaleMidi( melBase, degree );\n\t\t\t// The degree snap chose WHICH chord tone; this puts the note on the pitch the chord\n\t\t\t// actually sounds, which is not the same thing on every degree (see NearestSoundingTone).\n\t\t\tif ( resolve ) midi = NearestSoundingTone( midi, chord, h.Tick );\n\t\t\t// Where this note sits in the TUNE, which is the phrase a bend leans into. The tune's\n\t\t\t// own length is the cycle, so this is the same 0..1 whatever bar the section is on.\n\t\t\tfloat pu = ((h.Tick - anchor) % tune.LengthTicks + tune.LengthTicks)\n\t\t\t\t\t% tune.LengthTicks / (float)tune.LengthTicks;\n\t\t\tvar vc = Roll( ex, midi, prevMidi, exprRng, (float)_time.SpanSeconds( h.Tick, len ),\n\t\t\t\t\tBendBias( len, pu ) );\n\t\t\tprevMidi = midi;\n\t\t\tRenderLeadNote( _time.TickToSample( h.Tick ), _time.SpanSamples( h.Tick, len * 0.92 ),\n\t\t\t\tmidi, amp * NoteGain( h.Vel ), _time.SpanSeconds( h.Tick, len ) * 0.8,\n\t\t\t\tdrive, vc );\n\n\t\t\t// The genre's own hand on the same tune: country punctuates it with double-stops, metal\n\t\t\t// runs between its notes. The line is the same either way \u2014 this is ORNAMENT, not a\n\t\t\t// different melody, which is the difference between a genre playing a song and a genre\n\t\t\t// having its own song. Ornament also means occasional: harmonising every long note in\n\t\t\t// parallel thirds replaces the melody with a two-note chord (see EmitDoubleStop).\n\t\t\tif ( _prof.Lead == LeadStyle.DoubleStop && len >= Timing.TicksPerEighth * 2\n\t\t\t\t&& rng.Chance( DoubleStopChance ) )\n\t\t\t\tEmitDoubleStop( h.Tick, len, degree, amp * NoteGain( h.Vel ) );\n\t\t\telse if ( _prof.Lead == LeadStyle.Shred && len >= Timing.TicksPerBeat && rng.Chance( 0.18f ) )\n\t\t\t\tfor ( int k = 1; k <= 3; k++ )\n\t\t\t\t{\n\t\t\t\t\tint m2 = ScaleMidi( melBase, degree + k );\n\t\t\t\t\tRenderLeadNote( _time.EvenSpan( h.Tick + len / 2, len / 2, (k - 1) / 3.0 ),\n\t\t\t\t\t\t_time.SpanSamples( h.Tick, len / 8.0 ), m2, amp * 0.8f * NoteGain( h.Vel ),\n\t\t\t\t\t\t_time.SpanSeconds( h.Tick, len / 8.0 ) * 0.8, drive, vc );\n\t\t\t\t}\n\t\t}\n\t}\n}\n"
        },
        {
            "Ident": "gamah.skafinity",
            "Path": "Engine/Synth/Patch.cs",
            "FileName": "Patch.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 341414,
            "IsPrivate": false,
            "Code": "using System;\nusing System.Collections.Generic;\n\nnamespace Skafinity;\n\n// Patch \u2014 the subtractive voice definition every pitched note is rendered through:\n// unison oscillators \u2192 optional high-pass \u2192 resonant low-pass with a cutoff envelope.\n//\n// Part of the MusicGen engine \u2014 see MusicGen.cs.\n\n// \u2500\u2500 Synth core: unison osc \u2192 optional high-pass \u2192 resonant low-pass (cutoff\n//    envelope) \u2192 soft drive \u2192 AD/sustain amp env. \u2500\u2500\nstruct Patch\n{\n\tpublic int Osc;        // 0 sine 1 saw 2 square 3 triangle\n\tpublic int Voices;\n\tpublic float Detune;   // cents\n\tpublic float Amp;\n\tpublic float Attack;   // sec\n\tpublic double Decay;   // sec (exp time constant)\n\tpublic float Sustain;  // 0..1 (only if Sustained)\n\tpublic bool Sustained;\n\tpublic float Cutoff;   // Hz low-pass\n\tpublic float CutEnv;   // Hz added at attack, decays with Decay\n\tpublic float Reso;     // SVF damping (lower = more resonance)\n\tpublic float Highpass; // Hz one-pole high-pass (0 = off)\n\tpublic float Drive;    // tanh\n\tpublic float Pan;      // -1..1\n\tpublic float Vibrato;  // Hz (rate of the pitch wobble)\n\tpublic float Breath;   // 0..1 noise mix (reeds)\n\t// \u2500\u2500 Expression (per-note pitch shaping; see Expression/Voicing) \u2500\u2500\n\tpublic float VibDepth;   // vibrato depth as a pitch fraction (0 \u2192 legacy 0.005 when Vibrato>0)\n\tpublic float BendSemis;  // pitch offset in semitones at note START, glides to 0 (bend-in / glide); \u2212ve starts below\n\tpublic float BendTime;   // 0..1 fraction of the note over which BendSemis glides to 0\n\tpublic float ScoopSemis; // height (semitones) of a mid-note bend-up-and-back hump (0 = none)\n\t// THE BEND \u2014 the one a listener would name as one, and the only gesture here that moves the\n\t// note AWAY from its pitch rather than easing onto it. Everything above is an approach: it\n\t// starts off-pitch and resolves. This starts ON pitch, pushes up by BendUpSemis, and stays\n\t// there \u2014 or comes back, if BendUpHold says how long to sit at the top first. All three times\n\t// are SECONDS, never a fraction of the note, so a bend is the same physical gesture whatever\n\t// the tempo and whatever the note length.\n\tpublic float BendUpSemis; // semitones bent UP part way through the note (0 = none)\n\tpublic float BendUpStart; // seconds into the note where the bend begins\n\tpublic float BendUpTime;  // seconds the bend takes to reach pitch (and to come back down)\n\tpublic float BendUpHold;  // seconds held at pitch before releasing; 0 = held to the end\n\tpublic float PhaseSeed;  // oscillator start phase (0..1); 0 = legacy in-phase start. Used to\n\t                         // decorrelate the two double-tracking takes (see RenderPatch).\n}\n\npublic sealed partial class MusicGen\n{\n}\n"
        },
        {
            "Ident": "gamah.skafinity",
            "Path": "Engine/Voices/Comp.cs",
            "FileName": "Comp.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 341414,
            "IsPrivate": false,
            "Code": "using System;\nusing System.Collections.Generic;\n\nnamespace Skafinity;\n\n// The chordal layer's dispatch: the genre's comp figure, played the genre's way.\n//\n// Part of the MusicGen engine \u2014 see MusicGen.cs.\n\npublic sealed partial class MusicGen\n{\n\t/// <summary>The main chordal voice for a bar. The FIGURE comes from the genre's table (a\n\t/// pattern with its own length, so a two-bar riff is a two-bar riff) and the STYLE says what\n\t/// the voice does with each hit. Three genres sharing one comping rhythm was the loudest\n\t/// duplication left in the engine \u2014 this is the seam that fixes it.</summary>\n\t/// <param name=\"loud\">The section is loud enough that the genre changes technique \u2014 see\n\t/// <see cref=\"GenreProfile.LoudComp\"/>. One voice, one chord, a different instrument gesture;\n\t/// the caller has already picked the matching figure.</param>\n\tvoid RenderCompVoice( int barTick, int to, int chord, Pattern fig, Rng rng, Rng exprRng,\n\t\tbool loud = false )\n\t{\n\t\t_compTrim = DensityTrim( fig, loud ? _prof.LoudCompFigures : _prof.CompFigures );\n\t\tvar hits = fig.Slice( barTick, to, _sectionTick, _feel );\n\t\t// Remember what the riff played: where the bass doubles it (metal, and punk's unison\n\t\t// option) it reads these onsets rather than a table of its own.\n\t\t_riffOnsets.AddRange( hits );\n\t\tTrace?.Add( TraceVoice.Comp, hits );\n\n\t\tswitch ( loud ? _prof.LoudComp : _prof.Comp )\n\t\t{\n\t\t\tcase CompStyle.Riff: RenderRiffBar( hits, chord, rng, exprRng ); break;\n\t\t\tcase CompStyle.BoomChick: RenderStrumBar( hits, chord, rng, exprRng ); break;\n\t\t\tcase CompStyle.Downstroke: RenderDownstrokeBar( hits, chord, rng, exprRng ); break;\n\t\t\tcase CompStyle.Gallop: RenderGallopBar( hits, chord, rng, exprRng ); break;\n\t\t\tcase CompStyle.Pad: RenderPadBar( hits, chord, rng, exprRng ); break;\n\t\t\tdefault: RenderSkankBar( hits, chord, rng, exprRng ); break;\n\t\t}\n\t}\n\n\t/// <summary>\n\t/// How far the drawn comp figure is trimmed for its own DENSITY, relative to the genre's table.\n\t///\n\t/// A section draws its figure from the genre's table and the figures are not equally dense, so\n\t/// how loud the backing sits was decided by a draw: rock's rhythm guitar measured 4 dB apart\n\t/// between the seed the suite balances on and the genre's own average. Nothing was watching it \u2014\n\t/// <c>--levels</c> averages the spread away and the suite's balance check is a single seed \u2014\n\t/// and 4 dB is a larger move than any of the balances it sits under, handed out at random.\n\t///\n\t/// The trim is EXACTLY the arithmetic part of that and no more. N onsets of an unrelated-phase\n\t/// voice sum incoherently, so the level of a figure grows as \u221aN: the trim is therefore\n\t/// \u221a(table mean \u00f7 this figure), which cancels that growth and nothing else. What survives is\n\t/// everything MUSICAL about the difference \u2014 the cells' own velocities, the genre's accent\n\t/// weights, the section's energy, and the plain fact that a busier bar has more attacks in it.\n\t/// A full compensation (exponent 1) would flatten the busy figure into the sparse one, which is\n\t/// the mistake the other repair \u2014 levelling the figures against each other \u2014 makes by design.\n\t///\n\t/// AND IT IS MEAN-PRESERVING, which is the difference between narrowing a spread and moving a\n\t/// balance. \u221a(mean \u00f7 d) is convex in d, so averaged over a genre's table it comes out above 1\n\t/// and every genre's comp would drift a little louder \u2014 a mix change smuggled in with a spread\n\t/// fix, and the `*Balance` values are measured numbers that would then be wrong. Dividing by the\n\t/// table's own average trim leaves the genre exactly where `--levels` measured it and moves only\n\t/// the seed-to-seed variation, which is the whole and only claim.\n\t///\n\t/// Clamped, because a table with one outlier figure should not push the whole genre's comp\n\t/// around, and because a trim is a correction rather than a mix control.\n\t/// </summary>\n\t/// <summary>How often a chordal voice plays its genre's flourish over a two-bar window instead\n\t/// of the figure it is on. One number for every genre: what makes a gesture read as one is that\n\t/// it is occasional, and how occasional is not something a genre has a strong opinion about \u2014\n\t/// where genres differ is in what the gesture IS, which is the pattern itself.</summary>\n\tconst float OrnamentChance = 0.22f;\n\n\tinternal static float DensityTrim( Pattern fig, Pattern[] table )\n\t{\n\t\tif ( table == null || table.Length < 2 ) return 1f;\n\t\tfloat d = fig.Count / (float)fig.LengthTicks;\n\t\tif ( d <= 0f ) return 1f;\n\t\tfloat mean = 0f;\n\t\tforeach ( var p in table ) mean += p.Count / (float)p.LengthTicks;\n\t\tmean /= table.Length;\n\t\tfloat norm = 0f;\n\t\tforeach ( var p in table )\n\t\t{\n\t\t\tfloat pd = p.Count / (float)p.LengthTicks;\n\t\t\tnorm += pd > 0f ? MathF.Sqrt( mean / pd ) : 1f;\n\t\t}\n\t\tnorm /= table.Length;\n\t\tif ( norm <= 0f ) return 1f;\n\t\treturn Math.Clamp( MathF.Sqrt( mean / d ) / norm, 0.75f, 1.3f );\n\t}\n\n\t/// <summary>How long a comp hit rings.\n\t///\n\t/// NOT simply \"until the next onset\". A figure with uneven gaps then produces a note with an\n\t/// uneven length every single bar \u2014 the \"short, longggg\" shape that made the backing read as\n\t/// one repeated cell however varied the figure was. A chord rings for up to two beats and a\n\t/// stab is a stab; past that the voice is silent and the next hit lands into space, which is\n\t/// what a played part actually sounds like.</summary>\n\tstatic int CompLen( int spanTicks, bool ring )\n\t\t=> Math.Max( 1, Math.Min( spanTicks, ring ? Timing.TicksPerBeat * 2 : Timing.TicksPerEighth ) );\n\n\t/// <summary>The second chordal voice \u2014 the keys/piano/synth layer, where the genre has one.\n\t/// It never doubles the main voice: rock's organ answers the riff's gaps, country's piano\n\t/// hits the backbeat the guitar leaves alone, pop's arp moves over a pad that does not.\n\t/// </summary>\n\tvoid RenderKeysVoice( int barTick, int to, int chord, Rng rng, Rng exprRng, bool ornament )\n\t{\n\t\tvar hits = (ornament ? _prof.KeysOrnament : _keysFig).Slice( barTick, to, _sectionTick, _feel );\n\t\tTrace?.Add( TraceVoice.Keys, hits );\n\t\tswitch ( _prof.Keys )\n\t\t{\n\t\t\tcase KeysStyle.HonkyTonk: RenderHonkyTonkBar( hits, chord, rng, exprRng ); break;\n\t\t\tcase KeysStyle.Arp: RenderArpBar( hits, chord, rng, exprRng ); break;\n\t\t\tdefault: RenderKeysStabBar( hits, chord, rng, exprRng ); break;\n\t\t}\n\t}\n}\n"
        },
        {
            "Ident": "gamah.skafinity",
            "Path": "UI/SkafinityMusicPanel.razor",
            "FileName": "SkafinityMusicPanel.razor",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 341414,
            "IsPrivate": false,
            "Code": "@using System\n@using System.Collections.Generic\n@using System.Linq\n@using Sandbox\n@using Sandbox.UI\n@namespace Skafinity\n@inherits PanelComponent\n@* DontExecuteOnServer: this panel is pure human-client UI (like SkafinityPlayer). A dedicated\n   server never renders it, so skip its lifecycle there. On a listen server the host is still a\n   player, so the host-client keeps the panel \u2014 this only sheds the headless case. *@\n@implements Component.DontExecuteOnServer\n\n@*\n\tThe optional drop-in settings board for the Skafinity music engine.\n\n\tAdd this PanelComponent to a GameObject under a ScreenPanel (or WorldPanel). It finds a\n\tSkafinityPlayer in the scene (or set Player explicitly) and offers the whole transport as UI \u2014\n\tyou don't have to wire anything. The board's visibility is host-driven: set IsOpen (or call\n\tToggle()) from your game \u2014 e.g. bind it to a hotkey or your own pause/menu UI. This component\n\tintentionally ships no launcher of its own, so it imposes nothing on the host's HUD \u2014 which also\n\tmeans a freshly-dropped panel shows nothing until you bind IsOpen. Run `skafinity_panel` in the\n\tconsole to see it before you have (SkafinityCommands.cs).\n\n\tThe engine needs nothing from this: SkafinityPlayer plays on its own. This board is pure\n\tconvenience for players who want to drive the station rather than tune it in the inspector.\n\n\tIt is drawn against the WEB WIDGET as its design (web/skafinity-element.js) so the two are one\n\tproduct with one set of habits, and the wording and the derived decisions both sides need live in\n\tSkafinityBoard rather than in either drawing of it. What is deliberately NOT shared is layout:\n\tRazor and the DOM lay out differently enough that a common description of a row would be a lowest\n\tcommon denominator of both.\n\n\tRe-theming: the board derives its whole palette from one colour. Set SkafinityTheme.Accent from\n\tyour game (e.g. to your own UI accent) and the board follows; leave it unset and it is neutral\n\tgray-on-black. SkafinityMusicPanel.razor.scss holds only the layout/type tokens.\n*@\n\n<root class=\"@( IsOpen ? \"open\" : \"\" )\">\n\t@if ( IsOpen )\n\t{\n\t@{\n\t\tvar cfg = Player?.EffectiveConfig();\n\t\tint genre = cfg?.Genre ?? 0;\n\t\tvar here = Player?.Playhead() ?? default;\n\t\tbool known = here.Duration > 0;\n\t}\n\t<div class=\"board\" style=\"background-color:@SkafinityTheme.Bg;\">\n\t\t<div class=\"header\">\n\t\t\t<div class=\"title\">MUSIC</div>\n\t\t\t<div class=\"close\" onclick=\"@Toggle\">\u2715</div>\n\t\t</div>\n\n\t\t@* \u2500\u2500 Transport \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\t\t   Two rows: the buttons, and the bar that says where in the song they are acting. *@\n\t\t<div class=\"transport\">\n\t\t\t<div class=\"row\">\n\t\t\t\t<div class=\"btn big\" style=\"@BtnStyle\" tooltip=\"@SkafinityBoard.Copy.PrevTitle\"\n\t\t\t\t\t onclick=\"@( () => Step( -1 ) )\">@SkafinityBoard.Copy.Prev</div>\n\t\t\t\t<div class=\"btn big primary\" style=\"@BtnOnStyle\" tooltip=\"@SkafinityBoard.Copy.PlayTitle\"\n\t\t\t\t\t onclick=\"@TogglePlay\">@( Playing ? SkafinityBoard.Copy.Pause : SkafinityBoard.Copy.Play )</div>\n\t\t\t\t<div class=\"btn big\" style=\"@BtnStyle\" tooltip=\"@SkafinityBoard.Copy.NextTitle\"\n\t\t\t\t\t onclick=\"@( () => Step( 1 ) )\">@SkafinityBoard.Copy.Next</div>\n\n\t\t\t\t<div class=\"now\" style=\"@LabelStyle\">@SkafinityBoard.Copy.NowPlaying<b style=\"@TextStyle\">@( Player?.N ?? 0 )</b></div>\n\n\t\t\t\t@* Playback stalled on the song you skipped to \u2014 as opposed to the silent background\n\t\t\t\t   look-ahead, which nobody needs telling about. *@\n\t\t\t\t@if ( Player?.IsBuffering == true )\n\t\t\t\t{\n\t\t\t\t\t<div class=\"bufstate\" style=\"@AccentTextStyle\">@SkafinityBoard.Copy.Generating( Player?.N ?? 0 )</div>\n\t\t\t\t}\n\n\t\t\t\t<div class=\"vol right\" style=\"@LabelStyle\">\n\t\t\t\t\[email protected]\n\t\t\t\t\t<SkafinitySlider Min=\"@(0f)\" Max=\"@(1.5f)\" Step=\"@(0.01f)\" FixedWidth=\"@(120f)\"\n\t\t\t\t\t\t\t\t\t Value=\"@( Player?.Volume ?? 1f )\"\n\t\t\t\t\t\t\t\t\t OnValueChanged=\"@( (float v) => SetVolume( v ) )\"></SkafinitySlider>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t@* The seek bar. The whole song is already in memory, so a scrub is a stream restart on\n\t\t\t   PCM we are holding rather than a fetch \u2014 which is why this is a plain slider and not a\n\t\t\t   loading affordance. It goes inert, rather than drawing against a guess, until the song\n\t\t\t   has been rendered and has a length worth stating. *@\n\t\t\t<div class=\"seek\">\n\t\t\t\t<div class=\"time\" style=\"@LabelStyle\">@SkafinityBoard.Time( ShownTime( here ), known )</div>\n\t\t\t\t<SkafinitySlider tooltip=\"@SkafinityBoard.Copy.SeekTitle\" Disabled=\"@( !known )\"\n\t\t\t\t\t\t\t\t Min=\"@(0f)\" Max=\"@(1000f)\" Step=\"@(1f)\"\n\t\t\t\t\t\t\t\t Value=\"@( known ? ShownRatio( here ) * 1000f : 0f )\"\n\t\t\t\t\t\t\t\t OnValueChanged=\"@( (float v) => Scrub( v / 1000f, here.Duration ) )\"></SkafinitySlider>\n\t\t\t\t<div class=\"time total\" style=\"@LabelStyle\">@SkafinityBoard.Time( here.Duration, known )</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t@* \u2500\u2500 The seed \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\t\t   TWO copy buttons, because \"share this\" means one of two things and neither can be\n\t\t   recovered from the other: the song, with everything it left to chance written down, or\n\t\t   the station as it stands, which keeps rolling for whoever is handed it. *@\n\t\t<div class=\"row seed-bar\">\n\t\t\t<TextEntry @ref=\"_seedEntry\" placeholder=\"@SkafinityBoard.Copy.SeedPlaceholder\" class=\"grow seed-input\" />\n\t\t\t<div class=\"btn primary\" style=\"@BtnOnStyle\" onclick=\"@PlayTyped\">@SkafinityBoard.Copy.SeedGo</div>\n\t\t\t<div class=\"btn\" style=\"@BtnStyle\" tooltip=\"@SkafinityBoard.Copy.CopySongTitle\"\n\t\t\t\t onclick=\"@CopySong\">@_copySongLabel</div>\n\t\t\t<div class=\"btn\" style=\"@BtnStyle\" tooltip=\"@SkafinityBoard.Copy.CopyStationTitle\"\n\t\t\t\t onclick=\"@CopyStation\">@_copyStationLabel</div>\n\t\t</div>\n\n\t\t@* \u2500\u2500 What plays \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\t\t   These are not knob controls \u2014 genre, reroll and shuffle change the seed, and tinker only\n\t\t   opens the box \u2014 so they live on the board itself. Putting them in the mixer made them look\n\t\t   like part of it AND hid them from everyone who never opened it. *@\n\t\t<div class=\"row what-plays\">\n\t\t\t<div class=\"label\" style=\"@LabelStyle\">@SkafinityBoard.Copy.Genre</div>\n\t\t\t@* The dropdown IS the seed's genre part: \"Random\" takes it out of the string so every\n\t\t\t   song rolls its own again, and without that entry there is no way back out of a genre\n\t\t\t   once one has been chosen. It reads as selected when nothing is pinned. *@\n\t\t\t<DropDown class=\"genre\" style=\"@BtnStyle\" Value=\"@GenreValue\" Options=\"@GenreOptions\"\n\t\t\t\t\t  ValueChanged=\"@( (string v) => PickGenre( v ) )\" />\n\t\t\t<div class=\"btn\" style=\"@BtnStyle\" tooltip=\"@SkafinityBoard.Copy.RerollTitle\"\n\t\t\t\t onclick=\"@RerollStation\">@SkafinityBoard.Copy.Reroll</div>\n\t\t\t<div class=\"btn toggle @( Shuffling ? \"on\" : \"\" )\" style=\"@( Shuffling ? BtnOnStyle : BtnStyle )\"\n\t\t\t\t tooltip=\"@SkafinityBoard.Copy.ShuffleTitle\"\n\t\t\t\t onclick=\"@ToggleShuffle\">@( Shuffling ? SkafinityBoard.Copy.ShuffleOn : SkafinityBoard.Copy.ShuffleOff )</div>\n\t\t\t<div class=\"btn toggle right @( _tinkering ? \"on\" : \"\" )\" style=\"@( _tinkering ? BtnOnStyle : BtnStyle )\"\n\t\t\t\t onclick=\"@ToggleTinker\">@( _tinkering ? SkafinityBoard.Copy.TinkerOpen : SkafinityBoard.Copy.Tinker )</div>\n\t\t</div>\n\n\t\t@* \u2500\u2500 The knobs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\t\t   Behind the tinker button. They are the deep end of the toy, and a wall of sliders is\n\t\t   otherwise the first thing anybody meets. *@\n\t\t@if ( _tinkering )\n\t\t{\n\t\t\t<div class=\"panel vibe\" style=\"@PanelStyle\">\n\t\t\t\t<div class=\"h2\" style=\"@LabelStyle\">@SkafinityBoard.Copy.VibeHeading</div>\n\t\t\t\t<div class=\"matrix\">\n\t\t\t\t\t<div class=\"mrow mhead\">\n\t\t\t\t\t\t<div class=\"mvoice\"></div>\n\t\t\t\t\t\t@foreach ( var h in SkafinityBoard.ColumnHeaders )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t<div class=\"mcell mhlabel\" style=\"@LabelStyle\">@h</div>\n\t\t\t\t\t\t}\n\t\t\t\t\t</div>\n\t\t\t\t\t@foreach ( var row in SkafinityBoard.Matrix( genre ) )\n\t\t\t\t\t{\n\t\t\t\t\t\t<div class=\"mrow\">\n\t\t\t\t\t\t\t<div class=\"mvoice\">@row.Voice</div>\n\t\t\t\t\t\t\t@for ( int col = 0; col < SkafinityBoard.ColumnHeaders.Length; col++ )\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tvar f = row.Cells[col];\n\t\t\t\t\t\t\t\t<div class=\"mcell\">\n\t\t\t\t\t\t\t\t\t@if ( f != null )\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t@Knob( f, cfg, SkafinityBoard.KnobLabel( f, col ) )\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t}\n\t\t\t\t</div>\n\n\t\t\t\t@* Only when there IS a global knob. They have all been retired to reserved wire slots\n\t\t\t\t   (tempo to GenreProfile, width and reverb to house config), and a heading over an\n\t\t\t\t   empty grid reads as a panel that failed to draw something. *@\n\t\t\t\t@if ( SkafinityBoard.Globals( genre ).Count > 0 )\n\t\t\t\t{\n\t\t\t\t\t<div class=\"glabel\" style=\"@LabelStyle\">@SkafinityBoard.Copy.GlobalHeading</div>\n\t\t\t\t\t<div class=\"global-grid\">\n\t\t\t\t\t\t@foreach ( var f in SkafinityBoard.Globals( genre ) )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t<div class=\"knob-cell\">@Knob( f, cfg, f.Name )</div>\n\t\t\t\t\t\t}\n\t\t\t\t\t</div>\n\t\t\t\t}\n\n\t\t\t\t@* The only two buttons that act on the sliders, and they are not two dice. \ud83c\udfb2 always\n\t\t\t\t   moves every knob, because it draws a fresh vibe and PINS it. \u21ba is the way back out \u2014\n\t\t\t\t   dragging a knob pins the whole vibe, so without it one accidental drag turns an\n\t\t\t\t   endless station into one song forever \u2014 and it is off when there is nothing pinned\n\t\t\t\t   rather than looking like a die that did nothing. *@\n\t\t\t\t<div class=\"row vibe-actions\">\n\t\t\t\t\t<div class=\"btn\" style=\"@BtnStyle\" tooltip=\"@SkafinityBoard.Copy.VibeRollTitle\"\n\t\t\t\t\t\t onclick=\"@RerollVibe\">@SkafinityBoard.Copy.VibeRoll</div>\n\t\t\t\t\t<div class=\"btn @( VibePinned ? \"\" : \"off\" )\" style=\"@BtnStyle\"\n\t\t\t\t\t\t tooltip=\"@SkafinityBoard.Copy.VibeRandomTitle\"\n\t\t\t\t\t\t onclick=\"@RollVibe\">@SkafinityBoard.Copy.VibeRandom</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t}\n\n\t\t@* \u2500\u2500 The playlist \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\t\t   Past \u00b7 now \u00b7 up next. A row addresses its song by POSITION, which is its slot on the\n\t\t   timeline; the number it SHOWS is the song's index in its own station, and under shuffle\n\t\t   those are different things. *@\n\t\t<div class=\"panel playlist-panel\" style=\"@PanelStyle\">\n\t\t\t<div class=\"h2\" style=\"@LabelStyle\">@SkafinityBoard.Copy.PlaylistHeading</div>\n\t\t\t<div class=\"playlist\">\n\t\t\t\t@foreach ( var e in Queue() )\n\t\t\t\t{\n\t\t\t\t\tvar ee = e;\n\t\t\t\t\t<div class=\"plrow @( e.Current ? \"now\" : \"\" ) @( e.Cached ? \"cached\" : \"\" ) @( e.Progress >= 0 ? \"gen\" : \"\" )\"\n\t\t\t\t\t\t style=\"@RowStyle( e )\">\n\t\t\t\t\t\t<div class=\"pllabel\" onclick=\"@( () => Player?.SeekTo( ee.Position ) )\">\n\t\t\t\t\t\t\t<div class=\"plcaret\">@SkafinityBoard.RowCaret( e )</div>\n\t\t\t\t\t\t\t<div class=\"plhash\">@SkafinityBoard.Copy.Hash</div>\n\t\t\t\t\t\t\t<div class=\"plnum\">@e.N</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"plgenre\" style=\"@LabelStyle\">@SkafinityBoard.GenreName( e.Genre )</div>\n\t\t\t\t\t\t<div class=\"plstatus\" style=\"@LabelStyle\">\n\t\t\t\t\t\t\t@if ( e.Progress >= 0 )\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t<div class=\"bar\"><div class=\"bar-fill\" style=\"@BarFillStyle( e.Progress )\"></div></div>\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\[email protected]( e )\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"pldl\" style=\"@LabelStyle\" tooltip=\"@SkafinityBoard.Copy.ExportTitle( e.N )\"\n\t\t\t\t\t\t\t onclick=\"@( () => Save( ee.Position ) )\">\u2b07</div>\n\t\t\t\t\t</div>\n\t\t\t\t}\n\t\t\t</div>\n\t\t\t<div class=\"row jump\" style=\"@LabelStyle\">\n\t\t\t\[email protected]\n\t\t\t\t<TextEntry @ref=\"_jumpEntry\" Numeric=\"@true\" class=\"jump-input\" />\n\t\t\t\t<div class=\"btn\" style=\"@BtnStyle\" onclick=\"@JumpTo\">@SkafinityBoard.Copy.JumpGo</div>\n\t\t\t\t<div class=\"btn right\" style=\"@BtnStyle\"\n\t\t\t\t\t onclick=\"@( () => Save( Player?.Position ?? 0 ) )\">@( _saving ? SkafinityBoard.Copy.ExportBusy : SkafinityBoard.Copy.Export )</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t@if ( _msg != null )\n\t\t{\n\t\t\t<div class=\"msg\" style=\"@AccentTextStyle\">@_msg</div>\n\t\t}\n\t</div>\n\t}\n</root>\n\n@code\n{\n\t/// <summary>The player this panel drives. Leave unset to auto-find a <see cref=\"SkafinityPlayer\"/>\n\t/// in the scene on start.</summary>\n\t[Property] public SkafinityPlayer Player { get; set; }\n\n\t/// <summary>Whether the settings board is showing. Host-driven \u2014 set it from your game (or\n\t/// call <see cref=\"Toggle\"/>) to wire the board to a hotkey / pause menu / your own button.\n\t/// This component ships no launcher of its own.</summary>\n\t/// <remarks>Deliberately NOT a <c>[Property]</c>: this is transient UI state. On a networked\n\t/// (<c>NetworkMode: Snapshot</c>) GameObject a serialized <c>[Property]</c> rides the late-join\n\t/// snapshot, so a client joining while the host has the board open would restore\n\t/// <c>IsOpen = true</c> \u2014 leaking the host's UI state and rendering the board open (and unstyled,\n\t/// since the panel is rebuilt mid-deserialize). Leaving it un-serialized keeps it host-driven\n\t/// from code while starting <c>false</c> on every client.</remarks>\n\tpublic bool IsOpen { get; set; }\n\n\tTextEntry _seedEntry;\n\tTextEntry _jumpEntry;\n\tbool _seedInit;\n\t// The station seed this panel last wrote into the box \u2014 what tells a stale box from a typed one.\n\tstring _seedShown;\n\t// Both copy buttons keep their own label so pressing one doesn't report \"copied!\" on the other.\n\tstring _copySongLabel = SkafinityBoard.Copy.CopySong;\n\tstring _copyStationLabel = SkafinityBoard.Copy.CopyStation;\n\tstring _msg;\n\t// The knob matrix is closed until asked for. Not a [Property] for the same reason IsOpen is not.\n\tbool _tinkering;\n\tbool _saving;\n\n\t// A DRAG, held. A slider reports every mouse-move and there is no \"let go\" event to wait for, so\n\t// seeking on each report would restart the stream at every pixel. The thumb is therefore followed\n\t// here and the transport is told once the drag has settled \u2014 which is the same one-seek-per-gesture\n\t// the web gets from listening to `change` rather than `input`.\n\tconst float ScrubSettle = 0.2f;\n\tfloat _scrubTo = -1f;\n\tTimeSince _scrubSince;\n\n\tprotected override void OnStart()\n\t{\n\t\tPlayer ??= Scene.GetAllComponents<SkafinityPlayer>().FirstOrDefault();\n\t\tif ( Player == null )\n\t\t\tLog.Warning( \"SkafinityMusicPanel: no SkafinityPlayer found in the scene \u2014 add one (or set Player).\" );\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\t// The text entry follows the station as it stands \u2014 not once at open, but whenever the station\n\t\t// moves out from under it. Reroll, a pasted seed and a genre pin all change what the station\n\t\t// IS, and a box still showing the previous one reads as a reroll that did nothing.\n\t\t//\n\t\t// Only ever overwrites text this panel wrote: the moment somebody types, the box is theirs and\n\t\t// a background change to the station leaves it alone rather than eating what they were typing.\n\t\tif ( !IsOpen ) { _seedInit = false; return; }\n\t\tif ( _seedEntry != null )\n\t\t{\n\t\t\tvar station = Player?.StationSeed ?? \"\";\n\t\t\tif ( !_seedInit || ( station != _seedShown && _seedEntry.Text == _seedShown ) )\n\t\t\t{\n\t\t\t\t_seedEntry.Text = station;\n\t\t\t\t_seedShown = station;\n\t\t\t\t_seedInit = true;\n\t\t\t}\n\t\t}\n\n\t\t// The held scrub, applied once the drag settles. See _scrubTo.\n\t\tif ( _scrubTo >= 0f && _scrubSince > ScrubSettle )\n\t\t{\n\t\t\tvar d = Player?.Playhead().Duration ?? 0;\n\t\t\tif ( d > 0 ) Player?.SeekWithin( _scrubTo * d );\n\t\t\t_scrubTo = -1f;\n\t\t}\n\t}\n\n\t// \u2500\u2500 Theme bindings \u2500\u2500\n\t// The palette is runtime (SkafinityTheme), so every fill and themed text colour is bound here as\n\t// an inline style rather than named in the .scss. The stylesheet owns every border in return \u2014\n\t// see the header comment there for why the two must not overlap.\n\t//\n\t// Neither a rule nor an inline style reaches inside a control this library did not write, which\n\t// is why the board's slider is one it does (SkafinitySlider) \u2014 and why the accent reaches the\n\t// sliders at all.\n\tstatic string LabelStyle => $\"color:{SkafinityTheme.TextDim};\";\n\tstatic string TextStyle => $\"color:{SkafinityTheme.Text};\";\n\tstatic string AccentTextStyle => $\"color:{SkafinityTheme.AccentCss};\";\n\tstatic string BtnStyle => $\"background-color:{SkafinityTheme.Cell}; color:{SkafinityTheme.Text};\";\n\tstatic string BtnOnStyle => $\"background-color:{SkafinityTheme.AccentBg}; color:{SkafinityTheme.Text};\";\n\tstatic string PanelStyle => $\"background-color:{SkafinityTheme.Cell};\";\n\tstatic string BarFillStyle( float p ) => $\"width:{SkafinityBoard.Percent( p )}; background-color:{SkafinityTheme.AccentCss};\";\n\n\t// A playlist row reads as one of three states, brightest first: the song playing now, a song\n\t// already rendered and waiting, anything else.\n\tstatic string RowStyle( SkafinityPlayer.QueueEntry e ) =>\n\t\te.Current ? $\"background-color:{SkafinityTheme.AccentBg};\"\n\t\t: e.Cached ? $\"background-color:{SkafinityTheme.CellFillSoft};\"\n\t\t: \"\";\n\n\tbool Playing => Player != null && !Player.IsPaused;\n\tbool Shuffling => Player?.Shuffle ?? false;\n\tbool VibePinned => Player?.VibePinned ?? false;\n\tbool GenreRolling => Player == null || !Player.GenrePinned;\n\n\t/// <summary>Open/close the settings board. Convenience for hosts that want to bind a single\n\t/// action; you can also set <see cref=\"IsOpen\"/> directly.</summary>\n\tpublic void Toggle()\n\t{\n\t\tIsOpen = !IsOpen;\n\t\tif ( !IsOpen ) _seedInit = false;\n\t}\n\n\t// \u2500\u2500 The playhead \u2500\u2500\n\t// Mid-drag the thumb the user is holding wins over the clock; every other moment reads the\n\t// transport. Both halves have to agree or the label counts up while the thumb sits still.\n\tfloat ShownRatio( SkafinityPlayer.SongPosition p ) => _scrubTo >= 0f ? _scrubTo : p.Ratio;\n\tdouble ShownTime( SkafinityPlayer.SongPosition p ) => _scrubTo >= 0f ? _scrubTo * p.Duration : p.Time;\n\n\tvoid Scrub( float ratio, double duration )\n\t{\n\t\tif ( duration <= 0 ) return;\n\t\t_scrubTo = Math.Clamp( ratio, 0f, 1f );\n\t\t_scrubSince = 0;\n\t}\n\n\tvoid TogglePlay() { Player?.TogglePlay(); _msg = null; }\n\tvoid Step( int d ) { Player?.StepN( d ); _msg = null; }\n\tvoid SetVolume( float v ) { if ( Player != null ) Player.Volume = v; }\n\n\t// \u2500\u2500 The seed \u2500\u2500\n\tvoid PlayTyped()\n\t{\n\t\tif ( Player == null ) { _msg = null; return; }\n\t\t// A seed that will not parse leaves playback exactly where it was and says why, rather than\n\t\t// starting something adjacent to what was typed.\n\t\t_msg = Player.PlaySeed( _seedEntry?.Text, out var error )\n\t\t\t? SkafinityBoard.Copy.Playing( Player.CurrentSeed ) : error;\n\t}\n\n\t// This song, with everything it left to chance written down \u2014 whoever is handed it hears THIS,\n\t// not whatever their own station rolls at that index.\n\tvoid CopySong()\n\t{\n\t\ttry { Clipboard.SetText( Player?.CurrentSeed ?? \"\" ); _copySongLabel = SkafinityBoard.Copy.Copied; }\n\t\tcatch { _copySongLabel = \"\u2014\"; }\n\t}\n\n\t// The seed as it stands: whatever this player left rolling keeps rolling for them too.\n\tvoid CopyStation()\n\t{\n\t\ttry { Clipboard.SetText( Player?.StationSeed ?? \"\" ); _copyStationLabel = SkafinityBoard.Copy.Copied; }\n\t\tcatch { _copyStationLabel = \"\u2014\"; }\n\t}\n\n\t// \u2500\u2500 What plays \u2500\u2500\n\t// \"\" is the Random entry \u2014 the genre is not in the seed, so every song rolls its own.\n\tstatic readonly List<Option> GenreOptions = BuildGenreOptions();\n\tstatic List<Option> BuildGenreOptions()\n\t{\n\t\tvar list = new List<Option> { new( SkafinityBoard.Copy.GenreRandom, \"\" ) };\n\t\tfor ( int g = 0; g < VibeCodec.GenreCount; g++ )\n\t\t\tlist.Add( new Option( VibeCodec.Genres[g], g.ToString() ) );\n\t\treturn list;\n\t}\n\tstring GenreValue => GenreRolling ? \"\" : ( Player?.EffectiveConfig()?.Genre ?? 0 ).ToString();\n\n\tvoid PickGenre( string v )\n\t{\n\t\tif ( Player == null ) return;\n\t\tif ( string.IsNullOrEmpty( v ) ) { Player.RollGenre(); _msg = SkafinityBoard.Copy.GenreUnpinned; return; }\n\t\tif ( int.TryParse( v, out var g ) ) { Player.SetGenre( g ); _msg = null; }\n\t}\n\n\t// A different SONG, not a different taste: a fresh station at song 0, with anything pinned left\n\t// pinned.\n\tvoid RerollStation() { Player?.RerollStation(); _msg = SkafinityBoard.Copy.NewStation; }\n\n\tvoid ToggleShuffle() { if ( Player != null ) Player.SetShuffle( !Player.Shuffle ); _msg = null; }\n\n\tvoid ToggleTinker() { _tinkering = !_tinkering; }\n\n\t// \u2500\u2500 The knobs \u2500\u2500\n\t// One knob: a name/value header over a real slider (or a dropdown, where the field is a choice).\n\t// The whole layout comes from the library's field metadata for the current genre, so a new genre\n\t// \u2014 or a new knob \u2014 is a pure engine change and there is no field table here.\n\tRenderFragment Knob( VibeCodec.Field f, MusicGen.Config cfg, string label )\n\t{\n\t\tint genre = cfg?.Genre ?? 0;\n\t\tint idx = SkafinityBoard.FieldIndex( genre, f );\n\t\tfloat norm = cfg != null ? f.GetNorm( cfg ) : 0f;\n\t\treturn @<text>\n\t\t<div class=\"knob\">\n\t\t\t<div class=\"knob-head\">\n\t\t\t\t<div class=\"knob-name\" style=\"@LabelStyle\">@label</div>\n\t\t\t\t<div class=\"knob-val\" style=\"@AccentTextStyle\">@( cfg != null ? f.Display( cfg ) : \"\" )</div>\n\t\t\t</div>\n\t\t\t@if ( f.Choices != null )\n\t\t\t{\n\t\t\t\t<DropDown class=\"knob-select\" style=\"@BtnStyle\" Value=\"@SkafinityBoard.ChoiceIndex( f, norm ).ToString()\"\n\t\t\t\t\t\t  Options=\"@ChoiceOptions( f )\"\n\t\t\t\t\t\t  ValueChanged=\"@( (string v) => SetChoice( idx, f, v ) )\" />\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\t@* Snapped to the same discrete grid the seed encodes (one level per base-36 char), so\n\t\t\t\t   the slider can only land on values the vibe can actually represent. *@\n\t\t\t\t<SkafinitySlider Min=\"@(0f)\" Max=\"@( (float)(VibeCodec.Levels - 1) )\" Step=\"@(1f)\"\n\t\t\t\t\t\t\t\t Value=\"@( MathF.Round( norm * (VibeCodec.Levels - 1) ) )\"\n\t\t\t\t\t\t\t\t OnValueChanged=\"@( (float v) => SetVibe( idx, v / (VibeCodec.Levels - 1) ) )\"></SkafinitySlider>\n\t\t\t}\n\t\t</div>\n\t</text>;\n\t}\n\n\tstatic List<Option> ChoiceOptions( VibeCodec.Field f )\n\t{\n\t\tvar list = new List<Option>( f.Choices.Length );\n\t\tfor ( int k = 0; k < f.Choices.Length; k++ ) list.Add( new Option( f.Choices[k], k.ToString() ) );\n\t\treturn list;\n\t}\n\n\tvoid SetChoice( int idx, VibeCodec.Field f, string v )\n\t{\n\t\tif ( int.TryParse( v, out var k ) ) SetVibe( idx, SkafinityBoard.ChoiceNorm( f, k ) );\n\t}\n\n\tvoid SetVibe( int index, float norm )\n\t{\n\t\tif ( index < 0 ) return;\n\t\tPlayer?.SetVibe( index, norm );\n\t\t_msg = null;\n\t}\n\n\t// RerollVibe()'s defaults: the genre and the per-instrument volumes stay \u2014 the die over the\n\t// mixer re-voices the band, it does not swap the band or upend the mix you set.\n\tvoid RerollVibe() { Player?.RerollVibe(); _msg = SkafinityBoard.Copy.VibeRolled; }\n\n\tvoid RollVibe()\n\t{\n\t\tif ( Player == null || !Player.VibePinned ) return;   // nothing pinned \u2014 nothing to hand back\n\t\tPlayer.RollVibe();\n\t\t_msg = SkafinityBoard.Copy.VibeUnpinned;\n\t}\n\n\t// \u2500\u2500 The playlist \u2500\u2500\n\t// How many history / look-ahead entries to show either side of the current song.\n\tstatic int QueueBack => 3;\n\tstatic int QueueFwd => 5;\n\tIEnumerable<SkafinityPlayer.QueueEntry> Queue() =>\n\t\tPlayer?.Timeline( QueueBack, QueueFwd ) ?? Enumerable.Empty<SkafinityPlayer.QueueEntry>();\n\n\tvoid JumpTo()\n\t{\n\t\tif ( Player == null ) return;\n\t\tif ( int.TryParse( _jumpEntry?.Text, out var p ) ) Player.SeekTo( p );\n\t}\n\n\t// Rendering a song outside the cache takes seconds, so the button says so \u2014 a save that looks\n\t// like it did nothing is a save people press again.\n\tasync void Save( int position )\n\t{\n\t\tif ( Player == null || _saving ) return;\n\t\t_saving = true;\n\t\ttry\n\t\t{\n\t\t\tvar name = await Player.SaveToFileAsync( position );\n\t\t\t_msg = string.IsNullOrEmpty( name ) ? SkafinityBoard.Copy.SaveFailed : SkafinityBoard.Copy.Saved( name );\n\t\t}\n\t\tfinally { _saving = false; }\n\t}\n\n\tprotected override int BuildHash()\n\t{\n\t\t// Fold in the playhead and the queue's cached/generating state so the board animates as the\n\t\t// song runs and as songs render. Both are QUANTISED: the seek bar wants to move, but a panel\n\t\t// that rebuilds every frame to advance a bar by a pixel costs more than it shows. A fifth of\n\t\t// a second is smooth to look at and cheap to draw.\n\t\tvar q = new HashCode();\n\t\tq.Add( IsOpen ); q.Add( Player?.CurrentSeed ); q.Add( Player?.CurrentVibe );\n\t\tq.Add( Player?.Enabled ?? true ); q.Add( Player?.Volume ?? 1f );\n\t\tq.Add( Player?.GenrePinned ?? false ); q.Add( Player?.VibePinned ?? false );\n\t\tq.Add( Player?.Shuffle ?? false ); q.Add( Player?.IsPaused ?? false );\n\t\tq.Add( _tinkering ); q.Add( Player?.IsBuffering ?? false ); q.Add( _saving );\n\t\tq.Add( _msg ); q.Add( _copySongLabel ); q.Add( _copyStationLabel );\n\t\tq.Add( _scrubTo );\n\t\t// The palette rides in inline style= values, so the board has to rebuild when the host\n\t\t// retints it \u2014 nothing else in this hash moves when only SkafinityTheme.Accent changes.\n\t\tq.Add( SkafinityTheme.Accent );\n\t\tif ( IsOpen )\n\t\t{\n\t\t\tvar here = Player?.Playhead() ?? default;\n\t\t\tq.Add( (int)(here.Time * 5) ); q.Add( (int)(here.Duration * 5) );\n\t\t\tforeach ( var e in Queue() )\n\t\t\t{\n\t\t\t\tq.Add( e.N ); q.Add( e.Position ); q.Add( e.Cached ); q.Add( e.Current ); q.Add( e.Genre );\n\t\t\t\tq.Add( e.Progress >= 0 ? (int)MathF.Round( e.Progress * 20 ) : -1 );\n\t\t\t}\n\t\t}\n\t\treturn q.ToHashCode();\n\t}\n}\n"
        }
    ]
}