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.
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&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
}