Code/Engine/MusicGen.cs

MusicGen partial class for procedural music generation. Holds per-song state, constructors, public entry points for full or chunked generation, audition helpers, diagnostics (Explain, Onsets, etc.), and many fields controlling synthesis, drum kit, groove, form and rendering state.

File AccessNetworking
using System;
using System.Collections.Generic;
using static Skafinity.Osc;

namespace Skafinity;

/// <summary>
/// Procedural song generator — ska, rock, country, metal, punk and pop.
///
/// The project is named for where it started (ska + infinity) and ska is still genre 0, but the
/// engine composes six genres and none of them is the default case: what a genre plays comes out
/// of its own GenreProfile (form, comp figures, grooves, harmony tables, lead grammar).
///
/// A seed string ("{tag}:{n}") seeds a portable PRNG (xmur3 → mulberry32); the PRNG drives
/// every musical choice — tempo, key, progression, bass / skank / organ / lead / drum
/// patterns — so within one build the same seed always yields the same song. Output is
/// interleaved stereo 16-bit PCM (for SoundStream / Web Audio) or a WAV (debug/export).
///
/// SCOPE OF THAT GUARANTEE: one build. The s&amp;box library and the web wasm bundle compile
/// this same source, so they agree with each other — that is the parity that matters, and it
/// is structural rather than something to verify. Across commits, audio is EXPECTED to change
/// whenever the engine does; there is no golden-audio contract and no back-compat for old
/// seeds. See PLAN.md.
///
/// Synthesis: subtractive — unison-detuned oscillators through a resonant low-pass
/// state-variable filter with a cutoff envelope (warm, not "8-bit"); full synth drum kit
/// (kick/snare/toms/hats/crash + fills). Default voicing aims for a Sublime vibe: laid-back
/// reggae-rock tempo, bass-forward, prominent clean skank + organ bubble.
///
/// This class is split across Code/Engine/ — one partial per concern. This file holds the
/// per-song state every other partial reads, the constructor, and the public entry points
/// (whole-song and chunked). The engine stays framework-free (System, System.Collections
/// .Generic, System.Text only): no Sandbox.* and no web/Emscripten-isms, which is what lets
/// the one source compile to both targets.
/// </summary>
public sealed partial class MusicGen
{
	readonly Config _c;
	readonly int _sr;
	readonly float _drumGain;   // master kit gain — straight 0..1.5 slider × Config.KitPresence baseline
	float[] _bufL, _bufR;

	MusicGen( Config c ) { _c = c ?? new Config(); _sr = _c.SampleRate; _drumGain = Math.Clamp( _c.DrumVol, 0f, 1.5f ) * _c.KitPresence; }

	public const int Channels = 2;

	public static byte[] Generate( string tag, Config cfg = null )
	{
		var g = new MusicGen( cfg );
		return g.EncodeWav( g.Compose( tag ) );
	}

	public static short[] GenerateSamples( string tag, Config cfg, out int sampleRate )
	{
		var g = new MusicGen( cfg );
		float gain = g.Compose( tag );
		sampleRate = g._sr;
		return g.ToShorts( gain );
	}

	// ── Chunked generation (parallel synthesis) ──
	// Composition + drum synthesis are sequential (RNG-bound); pitched-voice synthesis
	// pulls no RNG, so the caller can split it across worker threads. Flow:
	//   var g = MusicGen.BeginPlan( tag, cfg );            // sequential plan + drums
	//   parallel-for window in 0..g.TotalSamples: g.RenderPitchedRange( from, to );
	//   short[] pcm = g.FinishStereo();                    // master + interleave
	public static MusicGen BeginPlan( string tag, Config cfg )
	{
		var g = new MusicGen( cfg );
		g.ComposePlan( tag );
		return g;
	}

	/// <summary>As <see cref="BeginPlan(string,Config)"/>, with every voice's onsets recorded as it
	/// plays them (see <see cref="PlanTrace"/>). The trace has to be attached BEFORE the plan runs,
	/// which is the whole reason this overload exists.</summary>
	internal static MusicGen BeginPlan( string tag, Config cfg, PlanTrace trace )
	{
		var g = new MusicGen( cfg ) { Trace = trace };
		g.ComposePlan( tag );
		return g;
	}

	public int TotalSamples => _bufL?.Length ?? 0;
	public int SampleRate => _sr;

	/// <summary>Master-normalize and interleave to stereo 16-bit PCM. Call after every
	/// <see cref="RenderPitchedRange"/> window has finished.</summary>
	public short[] FinishStereo() => ToShorts( Master() );

	/// <summary>What this song's composer decided — one line per choice, plus the form. Written
	/// for the "this seed sounds wrong" case: reading the decisions beats inferring them from the
	/// audio. Call after <see cref="BeginPlan"/>.</summary>
	internal string Explain()
	{
		var sb = new System.Text.StringBuilder();
		// The genre's own bands ride along, so a tempo can be read against what the genre plays
		// rather than in isolation. The DRAWN tempo, not the first section's. They differ by that section's TempoMul, and the
		// drawn one is the number every tempo decision reads.
		// Swing reads as STRAIGHT rather than "0.00": a song either swings or it does not, and a
		// number that can be zero invited reading a very small one as "swings a little" — which is
		// exactly the mistake the SwingChance draw exists to make unrepresentable.
		sb.AppendLine( $"tempo     {_bpm} bpm{(_fast ? " (uptempo band)" : "")}, "
			+ $"{(_time.Swing <= 0f ? "straight" : $"swing {_time.Swing:0.00}")}"
			+ $"{(_time.Swing >= _prof.ShuffleMin && _prof.ShuffleChance > 0 ? " — SHUFFLE" : "")}"
			+ $" [genre plays {_prof.BpmMin}–{_prof.BpmMax}, uptempo {_prof.FastBpmMin}–{_prof.FastBpmMax}]" );
		sb.AppendLine( $"key       root midi {_rootMidi}, scale [{string.Join( " ", _scale )}]" );
		sb.AppendLine( $"changes   [{string.Join( " ", _prog )}] at {_chordBars} bar(s)/chord, voicing [{string.Join( " ", _voicing )}]" );
		sb.AppendLine( _susVoice >= 0
			? $"sus       voice {_susVoice} resolves to the third half way through each chord"
			  + $" -> [{string.Join( " ", _voicingRes )}]"
			: "sus       none (the voicing states its third)" );
		// What each chord's inversion cost the voices: the octave each one was shifted so the chord
		// lands near the one before it. All zeros means the changes needed no re-voicing.
		var vl = new string[_vlShift.Length];
		for ( int c = 0; c < _vlShift.Length; c++ )
			vl[c] = $"[{string.Join( " ", _vlShift[c] )}]";
		sb.AppendLine( $"voicelead {string.Join( " ", vl )} (semitones per voice, per chord)" );
		sb.AppendLine( $"groove    {_songGroove.Name} (the song's — each section draws its own, below),"
			+ $" ride pref {_ridePref:0.00}, kit {(_kitLeads ? "leads" : "follows the band")}" );
		sb.AppendLine( $"parts     comp {_songComp.LengthTicks / _time.BarTicks} bar(s), bass {_songBass.LengthTicks / _time.BarTicks} bar(s)"
			+ $"{(_songKeys != null ? $", keys {_songKeys.LengthTicks / _time.BarTicks} bar(s)" : "")}"
			+ $"{(_songLoud != null ? $", loud comp {_songLoud.LengthTicks / _time.BarTicks} bar(s) as {_prof.LoudComp} from energy {_prof.LoudFrom:0.00}" : "")}"
			+ $"{(_riffBass ? ", bass doubles the riff" : "")}" );
		sb.AppendLine( $"tunes     chorus {(_chorusTune == null ? "—" : $"{_chorusTune.LengthTicks / _time.BarTicks} bars, {_chorusTune.Count} notes")}"
			+ $" | verse {(_verseTune == null ? "—" : $"{_verseTune.LengthTicks / _time.BarTicks} bars, {_verseTune.Count} notes")}"
			// Whether the tune is a PERIOD or a plain call and answer, and how long one phrase of it
			// is. A listening note about a melody is nearly always about how often it comes round.
			+ $" | {(_chorusTune != null && _chorusTune.LengthTicks > 2 * _tunePhraseTicks ? "period" : "call+answer")}"
			+ $", {_tunePhraseTicks / _time.BarTicks}-bar phrases" );
		sb.AppendLine( $"ending    {_ending}" );
		sb.AppendLine( $"ska bits  horns {_hasHorns}, organ {_organBubble}, lead voice {_lead}" );
		sb.AppendLine( "form" );
		var structure = _form;
		for ( int i = 0; i < structure.Count; i++ )
		{
			var p = structure[i];
			sb.AppendLine( $"  {i,2} {p.Type,-10} {p.Bars,2} bars  energy {p.Energy:0.00}  feel {p.Feel:0.0}"
				+ $"{(p.KeyShift != 0 ? $"  key +{p.KeyShift}" : "")}"
				+ $"{(p.Hemiola ? "  hemiola" : "")}{(p.BarBeats != null ? "  short bar" : "")}"
				+ $"  tune {(TuneFor( p.Type ) != null ? "yes" : "no")}"
				// Which cymbal the hand is on. Drawn per SECTION against the song's ride
				// preference, so it is not derivable from the genre or the seed's knobs — and it is
				// the first thing to check when a listening note is about a cymbal, because "the
				// ride is too loud" and "the crash is too loud" are different repairs and a section
				// on the hats is neither.
				+ $"  {CymbalHand( i )}  {(i < _sections.Count ? _sections[i].Groove : "?")}" );
		}
		return sb.ToString();
	}

	// The cymbal the hand was on for section i, as recorded while it rendered.
	string CymbalHand( int i ) =>
		i < 0 || i >= _sections.Count ? "?"
		: _sections[i].CrashRide ? "crash-ride"
		: _sections[i].Ride ? "ride"
		: "hats";

	/// <summary>
	/// The rendered mix's level BEFORE the master bus — peak, and RMS over everything above
	/// silence.
	///
	/// This is the instrument the per-voice <c>*Balance</c> values are tuned with, and the reason
	/// it exists: the master bus peak-normalizes, so rendering one voice on its own and measuring
	/// the OUTPUT tells you nothing about how loud that voice sits in a mix — every solo comes
	/// back normalized to the same peak. Measure here, between the render and the master.
	/// Call after <see cref="RenderPitchedRange"/>, instead of <see cref="FinishStereo"/>.
	/// </summary>
	/// <summary>Every pitched onset this song emitted, as sample positions, with the bar grid to
	/// measure them against. Solo a voice (mute the rest) and these are that voice's onsets —
	/// which is how "some parts are not sharing the downbeat" gets diagnosed as a number instead
	/// of argued about by ear. Drums are not here: they are synthesised straight into the buffer.
	/// </summary>
	internal (int[] Starts, int[] BarLines) Onsets()
	{
		// Silent events are skipped, so muting every voice but one really does isolate that voice
		// (the mix mutes by amplitude — the notes are still composed).
		var starts = new List<int>();
		foreach ( var e in _events ) if ( e.P.Amp > 0f ) starts.Add( e.Start );
		starts.Sort();

		var bars = new List<int>();
		var structure = _form;
		int tick = 0;
		foreach ( var part in structure )
			for ( int bar = 0; bar < part.Bars; bar++ )
			{
				bars.Add( _time.TickToSample( tick ) );
				tick += BarBeats( part, bar, _time.BeatsPerBar ) * Timing.TicksPerBeat;
			}
		return (starts.ToArray(), bars.ToArray());
	}

	/// <summary>Every position on the song's TICK grid, in samples, with the swing warp and the
	/// tempo curve already applied — i.e. exactly where a note is allowed to land. Compare onsets
	/// against this and a part that has drifted is a number, not an argument.
	///
	/// The tick grid, not the sixteenth grid: 48 ticks to the beat is what makes 8ths, 16ths and
	/// both triplet rates exact (see Timing), so a triplet ornament is ON the grid and a
	/// sixteenth-only ruler would flag it as drift.</summary>
	/// <summary>The genre this plan was composed for (diagnostics).</summary>
	internal int Genre => _genre;

	/// <summary>What the song's CHORUSES play — the draws that are the song's rhythm-section
	/// identity. A sweep counts distinct combinations of these, which is the number that says how
	/// many different rhythm sections a genre can produce at all: they come out of tables, so it is
	/// a table-size ceiling rather than anything randomness can reach.</summary>
	internal (Pattern Comp, Pattern Keys, Pattern Bass, DrumGroove Groove) SongParts =>
		(_songComp, _songKeys, _songBass, _songGroove);

	/// <summary>Whether this song's band wrote to the kit or the kit to the band. Cohesion is
	/// achieved by two different mechanisms depending on the answer, so a sweep that averages the
	/// two describes neither.</summary>
	internal bool KitLeads => _kitLeads;

	/// <summary>The song's two tunes (diagnostics — see <see cref="Melody"/>).</summary>
	internal (Pattern Chorus, Pattern Verse) Tunes => (_chorusTune, _verseTune);

	/// <summary>THIS SONG's form. One accessor rather than five call sites re-deriving it: a form
	/// that varies per song must be the same list everywhere, or the diagnostics' bar rulers
	/// disagree with the song that was rendered.</summary>
	internal IReadOnlyList<Part> Form => _form;

	/// <summary>Every audible note as (sample start, frequency), in composition order. The
	/// per-voice score behind the <c>--score</c> diagnostic: solo a voice, read what it actually
	/// played and where. Double-tracking emits two takes per note, so a caller that wants NOTES
	/// rather than takes de-duplicates on (start, freq).</summary>
	internal (int Start, float Freq)[] AudibleNotes()
	{
		var list = new List<(int, float)>();
		foreach ( var e in _events ) if ( e.P.Amp > 0f ) list.Add( (e.Start, e.Freq) );
		return list.ToArray();
	}

	/// <summary>First tick of each bar in the song — the ruler the score diagnostic reads
	/// against, and the one place the anomalous-measure bar lengths are honoured.</summary>
	internal int[] BarTickLines()
	{
		var bars = new List<int>();
		int tick = 0;
		foreach ( var part in _form )
			for ( int bar = 0; bar < part.Bars; bar++ )
			{
				bars.Add( tick );
				tick += BarBeats( part, bar, _time.BeatsPerBar ) * Timing.TicksPerBeat;
			}
		return bars.ToArray();
	}

	internal int[] GridSamples()
	{
		var grid = new List<int>();
		var structure = _form;
		int tick = 0;
		const int step = 1;
		foreach ( var part in structure )
			for ( int bar = 0; bar < part.Bars; bar++ )
			{
				int len = BarBeats( part, bar, _time.BeatsPerBar ) * Timing.TicksPerBeat;
				for ( int t = 0; t < len; t += step ) grid.Add( _time.TickToSample( tick + t ) );
				tick += len;
			}
		return grid.ToArray();
	}

	/// <summary>A generator with a buffer and a time base but NO SONG — the kit voices can be
	/// driven straight into it. This is what the <c>--audition</c> diagnostic renders each of its
	/// lines in: composition is skipped entirely, so what comes out is one drum voice and nothing
	/// else. Harness-only, like <see cref="RawLevels"/>, <see cref="AudibleNotes"/>,
	/// <see cref="GridSamples"/> and <see cref="Explain"/>.
	///
	/// DRY IS THE POINT, so everything the composer would normally lean on the kit with is set
	/// neutral here: no tone lean, no genre mix trim, no swing, no kit push, centred. Position is
	/// the one axis a line can ask for back, and it asks by setting <see cref="AuditionPan"/> —
	/// which is the same field the STEREO WIDTH slider drives, so a pan line is auditioning the
	/// real mechanism rather than a stand-in.
	///
	/// The caller reads <see cref="AuditionBuffers"/> and does NOT call <c>Master()</c>: the
	/// master bus peak-normalizes, and a per-line normalize would return every candidate at the
	/// same level and quietly delete the whole velocity half of the script.</summary>
	internal static MusicGen ForAudition( Config c, double seconds, int bpm )
	{
		var g = new MusicGen( c );
		g._genre = 1;
		g._prof = GenreProfile.For( g._genre );
		int n = Math.Max( 1, (int)(g._sr * seconds) );
		double samplesPerTick = 60.0 / Math.Max( 1, bpm ) * g._sr / Timing.TicksPerBeat;
		int totalTicks = (int)(n / samplesPerTick) + Timing.TicksPerBeat * 4;
		g._time = new Timing( 4, totalTicks, samplesPerTick, swing: 0f, drumPush: 0, sampleRate: g._sr );
		g._bufL = new float[n];
		g._bufR = new float[n];
		g._drumLowMul = g._drumHighMul = g._midMul = 1f;
		g._drumPan = 0f;
		g._drumTone = 0.5f;
		g._energy = 1f;
		g._feel = 1f;
		g._barTick = 0;
		g._sectionTick = 0;
		g._crashBrightLeft = true;
		return g;
	}

	/// <summary>The audition's raw, pre-master buffers.</summary>
	internal (float[] L, float[] R) AuditionBuffers() => (_bufL, _bufR);

	/// <summary>The audition's time base — a line asks it for the sample position of a tick, the
	/// same way a voice does.</summary>
	internal Timing AuditionTiming => _time;

	/// <summary>The kit's stereo spread, for the lines that are about position. 0 is centred.
	/// </summary>
	internal float AuditionPan { get => _drumPan; set => _drumPan = value; }

	/// <summary>Which side the bright crash sits on — the two crashes land opposite each other,
	/// so this is how a line hears them as two cymbals rather than one.</summary>
	internal bool AuditionCrashBrightLeft { get => _crashBrightLeft; set => _crashBrightLeft = value; }

	/// <summary>
	/// Set the instance up as a PLAYABLE KIT: a genre's groove, its tom tuning, its four cymbals
	/// and a pedal figure. The audition's own rule is one voice per line, and this is what a line
	/// needs when the question is the opposite one — how a fill moves across the kit, or how the
	/// cymbal hand sits under a groove. Everything drawn here comes off fixed local streams, so a
	/// line is repeatable and nothing touches a song's composition.
	/// </summary>
	internal void AuditionKit( int genre )
	{
		_genre = Math.Clamp( genre, 0, GenreProfile.Count - 1 );
		_prof = GenreProfile.For( _genre );
		_groove = _prof.DrawGroove( new Rng( "audition:groove" ) );
		_tomKit = TomKit.Tuned( _prof.Toms, 48 );
		var cy = CymbalDraw.Default;
		_rideBow = BuildCymbal( CymbalBands.Bow( cy.RideSplash, cy.RideWash, cy.RideRing ), 0 );
		_rideBell = BuildCymbal( CymbalBands.Bell( ring: cy.BellRing, clang: cy.BellClang ), 1 );
		_crashBright = BuildCymbal( CymbalBands.CrashBright( cy.BrightSplash, cy.BrightRing ), 2 );
		_crashDark = BuildCymbal( CymbalBands.CrashDark( cy.DarkSplash, cy.DarkRing, cy.DarkWash ), 3 );
		var footRng = new Rng( "audition:foot" );
		_footCells = 0;
		for ( int i = 0; i < 8; i++ )
			if ( footRng.Chance( FootOccupancy[i] ) ) _footCells |= 1 << i;
	}

	/// <summary>Which instrument the cymbal hand is on, for the lines that are about exactly that.
	/// </summary>
	internal void AuditionCymbalHand( bool ride, bool crashRide )
	{
		_ride = ride || crashRide;
		_crashRide = crashRide;
	}

	internal string AuditionGrooveName => _groove?.Name ?? "—";

	/// <summary>What each section of the planned song turned out to be — where it sits in samples,
	/// and which instrument its cymbal hand took. A line that wants "a section where the drummer
	/// rides" cannot ask for one directly: riding is a per-section roll against a per-song
	/// preference, so the only way to find one is to plan songs and look.</summary>
	internal readonly struct SectionInfo
	{
		public readonly int Start, End;
		public readonly bool Ride, CrashRide;
		public readonly string Type;
		/// <summary>The groove this section drew. Per SECTION now, so it is no more derivable from
		/// the genre or the seed's knobs than the cymbal hand is — and it is the other half of the
		/// same answer when a listening note is about the drums.</summary>
		public readonly string Groove;
		public SectionInfo( int start, int end, bool ride, bool crashRide, string type, string groove )
		{ Start = start; End = end; Ride = ride; CrashRide = crashRide; Type = type; Groove = groove; }
	}

	readonly List<SectionInfo> _sections = new();
	internal IReadOnlyList<SectionInfo> AuditionSections => _sections;

	/// <summary>Whether this song's groove ever opens the hats. A line about open-and-closed hats
	/// needs a groove that HAS an open cell — metal's have none at all, so a metal verse is a
	/// perfectly good section and a useless demonstration.</summary>
	internal bool AuditionGrooveOpens
	{
		get
		{
			foreach ( var h in _groove.Cymbal.Slice( 0, _groove.Cymbal.LengthTicks ) )
				if ( h.Value == DrumGroove.Open ) return true;
			return false;
		}
	}

	/// <summary>One bar of the genre's groove, and one fill — the engine's own passes, so a line
	/// hears what a song hears rather than a hand-written imitation of it.</summary>
	internal void AuditionBar( int barTick, Rng noise )
		=> RenderDrumBar( barTick, _time.BarTicks, barTick + _time.BarTicks, noise );

	internal void AuditionFill( int fromTick, int toTick, Rng noise, Rng rng )
		=> RenderFill( fromTick, toTick, noise, rng );

	/// <summary>The kit's cymbals, for the lines that play one directly — through the SAME bus the
	/// groove uses. An audition line that invents its own balance is not auditioning the thing the
	/// song plays: the ride and the hats sit on different buses, so comparing them at a made-up
	/// gain answers nothing.</summary>
	internal void AuditionCymbalHit( int which, int at, float amp, int chokeAt = int.MaxValue,
		float chokeTau = HandChoke )
	{
		var t = which switch { 0 => _rideBow, 1 => _rideBell, 2 => _crashBright, _ => _crashDark };
		if ( which <= 1 ) RenderRideCym( at, amp, t, chokeAt, chokeTau );
		else RenderCrashCym( at, amp, t, dark: which == 3, chokeAt, chokeTau );
	}

	internal (float Peak, double Rms) RawLevels()
	{
		float peak = 0; double sum = 0; int n = 0;
		for ( int i = 0; i < _bufL.Length; i++ )
		{
			float a = Math.Max( MathF.Abs( _bufL[i] ), MathF.Abs( _bufR[i] ) );
			peak = Math.Max( peak, a );
			if ( a > 0.0005f ) { sum += (double)_bufL[i] * _bufL[i] + (double)_bufR[i] * _bufR[i]; n += 2; }
		}
		return (peak, n > 0 ? Math.Sqrt( sum / n ) : 0);
	}

	GenreProfile _prof;      // the genre's character table — every per-genre decision reads this
	int[] _scale, _prog;
	int[] _voicing;          // the song's chord voicing, in scale-degree offsets (Harmony)
	int[] _voicingRes;       // the same voicing with any suspension resolved to the third; the SAME
	                         // array as _voicing when the voicing is not suspended
	int _susVoice = -1;      // index of the suspended voice in _voicing, or -1 (Harmony.SuspendedVoice)
	int _susResolveTick;     // tick the current chord's suspension resolves on (VoicingAt)
	int[][] _vlShift;        // per chord of _prog, the octave offset each voice takes so the chord
	                         // sits near the one before it (Harmony.PlanVoiceLeading)
	int[] _vlRot;            // per chord, which voicing offset each voice takes: voice i plays
	                         // offset (i + _vlRot[c]) mod n — the rotation half of the same plan
	int[] _endingPrev;       // pitches the ending's chord is voice-led out of; null before the
	                         // ending's first chord, which is then left in root position
	float _compTrim = 1f;    // the drawn comp figure's density trim (Comp.DensityTrim) — set per
	                         // section by RenderCompVoice and read by the comp voice's emitters
	int _rootMidi;
	Instrument _lead;
	float _leadPan;
	float _widthScale = 1f;  // STEREO WIDTH slider (PanAmount) as a 0..1 master: scales the drum
	                         // pan AND the double-tracking spread/decorrelation. 1 = full (design)
	                         // width; 0 = everything collapses to centre (mono).
	float _drumPan = DrumPan;// per-song effective drum spread = DrumPan * _widthScale
	bool _hasHorns;
	Pattern _hornFig;        // the horn section's 2-bar call-and-response figure
	Pattern _bassPat;        // the song's bass line — a Pattern, so it can be a 2- or 4-bar phrase
	Pattern _compFig;        // the main chordal voice's comp figure (the CURRENT section's)
	Pattern _keysFig;        // the second chordal voice's figure (null where the genre has none)
	bool _compOrn, _keysOrn; // this two-bar window plays the genre's flourish (see RenderComp)
	// The song's own figures — what its choruses play. Other sections draw their own against a
	// stream keyed by section type, so the backing contrasts instead of looping one cell all song.
	Pattern _songComp, _songKeys, _songBass;
	// The figure the main chordal voice plays in the song's LOUD sections, where the genre has a
	// loud comp at all (null otherwise). Drawn once per song rather than per section on purpose:
	// the loud sections are the choruses, and every chorus must agree — that is the song's hook.
	Pattern _songLoud;
	DrumGroove _groove;      // the CURRENT SECTION's groove — per-genre tables, not a shared switch default
	DrumGroove _songGroove;  // and the song's own, which every chorus plays
	// What the kit actually plays this section: the groove's patterns, worked on by the arranger.
	// Separate fields rather than a rebuilt DrumGroove so the groove stays the thing that was drawn
	// and these stay the thing that is played — the same split PlanTrace records.
	Pattern _kickFig, _snareFig;
	Pattern _songKick, _songSnare;   // …and the song's own, which every chorus replays
	bool _kitLeads;          // per song: does the band write to the kit, or the kit to the band
	bool _riffBass;          // the bass reads the riff's onsets instead of playing its own pattern
	EndingStyle _ending;     // how this song lands (see EndingStyle) — a per-song draw, not a fixed pad
	readonly List<Hit> _riffOnsets = new(); // this bar's riff, for the bass to double
	// Where every part's onsets get written when a sweep is watching (see PlanTrace). Null in
	// every ordinary render, so this is a null check per bar per voice and nothing else.
	internal PlanTrace Trace;
	bool _ride;              // per-SECTION: ride cymbal drives the eighth pulse instead of closed hats (set in RenderSection from _ridePref)
	float _ridePref;         // per-song lean toward riding the ride vs the hats; each section rolls its own _ride against this
	/// <summary>The band a song's reverb wet is drawn from. Not 0..1: bone dry and swimming are
	/// both reachable there and neither is a thing any of these genres is.</summary>
	internal const float ReverbMin = 0.15f, ReverbMax = 0.75f;
	float _reverbWet = 0.5f; // this song's room, drawn per song (see ComposePlan)
	bool _crashBrightLeft;   // per-song: which side the kit's two crashes sit on (bright crash left ⇄ dark crash right, or flipped)
	bool _crashRide;         // per-SECTION: the cymbal hand is on a crash rather than the ride (GenreProfile.CrashRideFrom)
	int _footCells;          // per-SECTION: the hi-hat pedal's own figure as an 8-bit eighth mask (measured — see FootOccupancy)
	TomKit _tomKit;          // per-song: the three tom pitches, tuned from the song's root by the genre's TomTune
	// The song's cymbals, each rendered once and stamped per hit (see CymbalTable). Built lazily,
	// because a song that never rides pays for no ride: they are the most expensive objects the
	// engine makes, and which of them a song needs is not known until its sections are rendered.
	// The song's four cymbals. Cheap structs now — seven bands and a low pair each — so they are
	// built with the rest of the plan rather than lazily behind a null check.
	float[][] _rideBow, _rideBell, _crashBright, _crashDark;
	// The kit's per-song nuance: a drum is a physical object and a band of values that all read
	// as the right drum is what nuance IS (see KitNuance), so the song draws from those bands
	// rather than the engine picking a point out of each one.
	KickTone _kickTone = KickTone.Default;
	HatTone _hatTone = HatTone.Default;
	HatTone _footTone = HatTone.Foot;
	bool _organBubble;
	bool _fast;
	int _bpm;                // the song's drawn tempo, after the TEMPO knob and the genre's own saturation
	int _genre;              // 0 ska, 1 rock, 2 country, 3 metal, 4 punk, 5 pop
	int _chordBars = 2;      // bars per chord — the genre's harmonic rhythm (GenreProfile.ChordBars)
	bool _hornLead;          // the lead line is the ska horn section rather than a lead guitar
	string _tag;             // the per-song seed string, reused to seed per-section streams
	Timing _time;            // the song's time base: eighth length, swing, kit push (see Timing.cs)
	float _drumTone = 0.5f;  // DrumTone 0..1 → toms↔cymbals CONTENT bias in fills/groove decoration
	float _drumLowMul = 1f;  // DrumTone + the genre mix trim → kick/tom/bass gain lean
	float _drumHighMul = 1f; // DrumTone + the genre mix trim → hat/cymbal gain lean
	float _midMul = 1f;      // the genre mix trim on the body of the mix (guitars, keys, horns)

	// ── per-SECTION state ──
	// Set once per section in RenderSection; every voice reads these instead of asking "am I in
	// a verse?" (see Part). This is what makes a chorus a chorus rather than a repeat.
	// THIS SONG's form, drawn once in ComposePlan and read everywhere. Five places used to derive
	// it from the genre alone; that was harmless while the answer was a constant and is a ruler for
	// a different song the moment it varies (see DrawForm).
	List<Part> _form = new();
	int[] _sectionStart = Array.Empty<int>(); // first tick of each section
	int _sectionTick;        // the current section's first tick — patterns loop from here
	int _sectionTicks;       // its length in ticks — a section shorter than the tune sings the
	                         // tune's resolving half rather than being cut off mid-phrase
	int _barTick;            // the current bar's first tick — the accent grid is relative to it
	float _energy = 1f;      // 0 = as thin as the arrangement gets, 1 = full band
	float _feel = 1f;        // pattern-rate multiplier: 0.5 half time, 2 double time
	int _keyShift;           // semitones this section is transposed by (the final-chorus lift)
	Section _sectionType;    // which kind of section is playing — voices that must not double the
	                         // tune (the ska horn section) ask TuneFor() about it
}