Engine/MusicGen.cs

Procedural music generator core state and public API. Holds per-song fields, constructors, and methods for full and chunked generation, auditioning kit/drums, diagnostics (Explain, Onsets, AudibleNotes, RawLevels), and various accessors for the composed plan and buffers.

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
}