Part of the MusicGen engine, this file contains the composition pass for generating a song: planning the song structure, drawing per-song and per-section RNG streams, arranging parts, and rendering sections into stereo sample buffers (drums, bass, comp, keys, leads, endings, etc.). It implements timing, forms, section rendering, dynamics and final tailing.
using System;
using System.Collections.Generic;
using static Skafinity.Osc;
namespace Skafinity;
// The composition pass. Plans the whole song (RNG draws + drum synthesis written straight
// into the buffers), then renders each section, each voice on its own per-section RNG
// stream keyed so a repeated section repeats rather than re-rolls.
//
// Part of the MusicGen engine — see MusicGen.cs.
public sealed partial class MusicGen
{
// A copy of an int[], spelled out. NOT `(int[])a.Clone()`: s&box compiles this same source
// against an API whitelist that DENIES Array.Clone specifically, so it is a compile error
// there (SB1000) while compiling fine here and in the wasm build — a build we cannot run, so
// the fix has to be a habit rather than a test.
//
// It is a deliberate carve-out rather than an omission [SOURCE, read 2026-08-03]:
// Sandbox.Access/Rules/Types.cs allows "System.Array*" and then denies
// "!System.Private.CoreLib/System.Array.Clone*" on the next line — one of only seven deny
// entries in the whole ruleset, sitting directly under the same treatment of
// Object.MemberwiseClone. So the rule is about that ONE MEMBER, not about System.Array:
// Array.Sort, Array.Empty and Array.Copy are all allowed and all used in this engine.
static int[] CopyOf( int[] src )
{
var d = new int[src.Length];
for ( int i = 0; i < src.Length; i++ ) d[i] = src[i];
return d;
}
// Single-threaded generation (used by Generate / GenerateSamples). The controller
// uses the chunked path instead (BeginPlan → parallel RenderPitchedRange → FinishStereo).
float Compose( string tag )
{
ComposePlan( tag );
RenderPitchedRange( 0, _bufL.Length );
return Master();
}
// Sequential planning pass: RNG composition + drum synthesis written straight into
// the buffer, while every pitched note is collected as an event (rendered later,
// possibly in parallel).
//
// EVERY GENRE PULLS THE SAME NUMBER OF VALUES out of this stream. A weighted draw is one
// Next() however the table is weighted, a genre with no second chordal voice still takes its
// figure draw, and the ska-only rolls (lead instrument, organ bubble, horn section) happen
// for everyone. A knob decides WHAT plays, never how many values the composer pulls.
void ComposePlan( string tag )
{
_events.Clear();
_chorusArranged = false;
int beatsPerBar = 4; // 4/4 today; Timing carries it so voices never assume
_tag = string.IsNullOrEmpty( tag ) ? "rotaliate" : tag;
_genre = Math.Clamp( _c.Genre, 0, GenreProfile.Count - 1 );
var prof = GenreProfile.For( _genre );
_prof = prof;
_chordBars = Math.Max( 1, prof.ChordBars );
_hornLead = prof.HornLead;
var rng = new Rng( _tag.ToLowerInvariant() );
// Whether this song takes the genre's uptempo band. The genre's own odds now (see
// GenreProfile.FastChance) rather than a knob's — one draw either way, so the draw count
// still cannot depend on the genre.
_fast = rng.Chance( prof.FastChance );
int bpm = _bpm = prof.DrawBpm( rng, _fast );
_scale = rng.PickWeighted( prof.Scales, prof.ScaleWeights );
_prog = rng.Pick( prof.Progressions );
// The song's chord vocabulary — a triad, a 7th, a sus, a bare power chord. Every chordal
// voice reads this one voicing, so the guitar and the keys agree about what the chord IS
// while still playing different rhythms.
_voicing = rng.PickWeighted( prof.Voicings, prof.VoicingWeights );
// A sus is the one voicing that is not a chord quality but a delayed third, so the song
// also carries the spelling it resolves to (see Harmony.SuspendedVoice). Not a draw and
// not a second voicing: it is the same chord with its suspension landing.
_susVoice = Harmony.SuspendedVoice( _voicing );
_voicingRes = _voicing;
if ( _susVoice >= 0 )
{
_voicingRes = CopyOf( _voicing );
_voicingRes[_susVoice] = Harmony.Third;
}
// How each chord inverts to stay near the one before it. A property of the changes and
// the voicing, so it is decided once and every chordal voice reads the same table — the
// guitar and the keys must agree on the inversion as much as on the chord. Costs no draw.
var plan = Harmony.PlanVoiceLeading( _scale, _prog, _voicing );
_vlShift = plan.Shift; _vlRot = plan.Rot;
_endingPrev = null;
_rootMidi = 28 + rng.Int( 8 ); // E1..B1 bass root
// Which horn/organ voice takes the ska lead, weighted by the config. Rolled for every
// genre so the draw count doesn't depend on the genre; only ska reads the result (the
// rest route the lead to a guitar in RenderLeadNote).
_lead = PickInstrument( rng );
_leadPan = (rng.Next() * 2f - 1f) * _c.PanAmount;
// NOTE the ceiling: a genre's Mix.Width above 1 cannot be reached at the design width, so
// pop's 1.2 lands at 1.0 like everyone else. The RELATIVE widths still hold (country 0.85
// sits inside pop's), which is what the profile is for.
_widthScale = Math.Clamp( _c.PanAmount * prof.Mix.Width * _c.GenreMix, 0f, 1f );
_drumPan = DrumPan * _widthScale;
_bassPat = _songBass = rng.Pick( prof.BassPatterns );
_compFig = _songComp = rng.Pick( prof.CompFigures );
// What the chordal voice plays where the section is loud enough to change technique. Drawn
// for every genre so the draw count does not depend on whether the genre has a loud comp;
// only the genres with one read the result (PickOrNull takes its value either way).
_songLoud = PickOrNull( rng, prof.LoudCompFigures );
// The second chordal voice's figure. Drawn even where the genre has none, so the genres
// that do have one are not the only ones consuming the value.
_keysFig = _songKeys = PickOrNull( rng, prof.KeysFigures );
_groove = _songGroove = prof.DrawGroove( rng );
_songKick = _songGroove.Kick; _songSnare = _songGroove.Snare;
// WHO GOES FIRST. Drawn once per song and for every genre, so the draw count does not depend
// on the answer: the band writing to the kit and the kit writing to the band are two
// different mechanisms for the same cohesion, and a genre has an opinion about which one it
// is (see GenreProfile.KitLeadsChance).
_kitLeads = rng.Chance( prof.KitLeadsChance );
// Metal's bass either pedals under the riff or doubles it; punk sometimes takes the same
// unison. Both are RELATIONAL, so this decides whether the bass reads the guitar's onsets
// at render time rather than which table it plays from.
_riffBass = rng.Chance( prof.RiffBassChance );
// Whether this song has an organ bubbling under the skank, and whether the horn section
// backs the lead. Both are ska arrangement choices the ORGAN BUBBLE / HORN SECTION knobs
// set the odds of; rolled for every genre for the same draw-count reason as the lead.
_organBubble = rng.Chance( _c.OrganBubbleChance );
_hasHorns = rng.Chance( _c.HornSectionChance );
_hornFig = HornFigure( rng, beatsPerBar );
// How much this song leans on the ride cymbal vs the closed hats for the main pulse.
// Every song can do both — each SECTION rolls its own choice against this preference.
_ridePref = prof.DrawRidePref( rng );
// Which side the two crashes sit on (±25%); flips per song so the stereo image varies.
// How much room this song sits in — per-song character drawn from a band, the way tempo and
// swing are, and then trimmed by the genre's own profile in Master(). A fixed value made
// every song of a genre sit in the identical space.
_reverbWet = ReverbMin + rng.Next() * (ReverbMax - ReverbMin);
_crashBrightLeft = rng.Chance( 0.5f );
// THE KIT IS SET UP ONCE, the way a drummer sets one up: three toms tuned in the genre's
// intervals from the song's own key, and the rack on one side or the other. Drawn from
// _rootMidi and never from a section's _keyShift — nothing re-reads it, so the toms cannot
// drift with a mid-song key change, and only the pitch CLASS is used so the set stays a
// drum-sized set whatever octave the song is written in.
_tomKit = TomKit.Tuned( prof.Toms, _rootMidi, rackLeft: rng.Chance( 0.5f ) );
// And the nuance the audition approved as BANDS rather than values (see KitNuance): the
// same drum does not make the identical sound twice, and which point of each band this
// kit sits at is as much a property of a song as its tempo is.
_kickTone = KickTone.Default.With(
clickCut: KitNuance.At( KitNuance.ClickCutMin, KitNuance.ClickCutMax, rng.Next() ),
jitter: 0.35f );
// The cymbals' own bands. Both audition rounds came back "all of these work", which is a
// finding about nuance rather than an undecided question — so the song draws a point out of
// each band instead of the engine picking one by ear and freezing it into every song.
var cy = CymbalDraw.Draw( rng );
_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 );
float hatU = rng.Next(), footU = rng.Next();
_hatTone = HatTone.Default.With(
openDur: KitNuance.At( KitNuance.OpenHatDurMin, KitNuance.OpenHatDurMax, hatU ),
openCut: KitNuance.OpenHatCut, decayFrac: 0.45 + 0.05 * hatU,
openCurve: KitNuance.HatOpenCurve );
_footTone = HatTone.Foot.With(
attackSec: KitNuance.At( KitNuance.FootAttackMin, KitNuance.FootAttackMax, footU ),
closedDur: KitNuance.At( KitNuance.FootDurMin, KitNuance.FootDurMax, footU ),
closedCut: KitNuance.At( KitNuance.FootCutMin, KitNuance.FootCutMax, 1f - footU ) );
// How the song lands. Every song used to end on the same fixed pad, whatever the genre.
_ending = rng.PickWeighted( prof.Endings, prof.EndingWeights );
// Swing is the genre's own feel, drawn per song from its band exactly the way tempo is —
// not a knob, so a reroll can never hand metal a shuffle. Ska-punk and country may instead
// draw a genuine 2:1 triplet shuffle, which is a different feel rather than more swing.
//
// DRAWN BEFORE THE TUNE, because the tune has to know. Under a shuffle the beat's own
// subdivision is the triplet, and a melody written in straight sixteenths against that is
// not syncopation, it is two grids at once — see Melody.Draw.
float swing = prof.DrawSwing( rng, _fast );
// The song's TUNE — the thing a listener hums back. Drawn off its own streams, so a song
// having a melody shifts nothing else in the composition.
DrawTunes( beatsPerBar * Timing.TicksPerBeat, swing > 0f );
double secPerEighth = 60.0 / bpm / 2.0;
int spe = (int)Math.Round( _sr * secPerEighth );
// Drum tone (toms↔cymbals) → per-voice gain split, and drive (pull↔push) → a constant
// kit timing bias (− = ahead/push, + = behind/lay back; 0.5 = dead on).
float dt = Math.Clamp( _c.DrumTone, 0f, 1f );
_drumTone = dt;
// Gentle gain lean (neutral at 0.5 so the balanced kit is untouched there), then the
// genre's own mix trim on top: metal dry and mid-scooped, pop bright, country centred.
_drumLowMul = (1.2f - 0.4f * dt) * MixTrim( prof.Mix.Low );
_drumHighMul = (0.7f + 0.6f * dt) * MixTrim( prof.Mix.High );
_midMul = MixTrim( prof.Mix.Mid );
int drumPush = (int)Math.Round( (0.5f - Math.Clamp( _c.DrumDrive, 0f, 1f )) * 2f * 0.13f * spe );
// Lay out the structure first — the time base is built over the song's full tick span,
// so it has to know how long the song is, and the sections carry the tempo curve.
// THE FORM IS DRAWN ONCE AND CACHED. Off its own stream, so a form that varies costs the
// song stream nothing and its draw-count rule holds unchanged; and cached because four
// diagnostics read it back to build bar rulers, and a ruler derived from a second draw is a
// ruler for a different song.
var structure = _form = DrawForm( _prof, new Rng( $"{_tag}:form" ) );
_sectionStart = new int[structure.Count];
int totalTicks = 0;
for ( int si = 0; si < structure.Count; si++ )
{
_sectionStart[si] = totalTicks;
totalTicks += SectionTicks( structure[si], beatsPerBar );
}
// The time base the whole band shares from here on. Ticks are the grid; the swing is a
// warp applied on the way out to samples, not a quantisation; and the per-tick delta
// carries the tempo CURVE — each section's own tempo, plus the ritard over the final
// bars (a song that just stops reads as a loop point, not an ending).
double baseDelta = spe / (double)Timing.TicksPerEighth;
int ritardTicks = Math.Min( totalTicks / 2, 2 * beatsPerBar * Timing.TicksPerBeat );
int ritardFrom = Math.Max( 0, totalTicks - ritardTicks );
var delta = new double[totalTicks + 2];
for ( int t = 0; t < delta.Length; t++ )
{
float mul = structure[SectionAt( t, structure.Count )].TempoMul;
double d = baseDelta / Math.Max( 0.5f, mul );
if ( t > ritardFrom && ritardTicks > 0 )
d *= 1.0 + RitardAmount * (t - ritardFrom) / (double)ritardTicks;
delta[t] = d;
}
_time = new Timing( beatsPerBar, totalTicks, t => delta[Math.Min( t, delta.Length - 1 )],
swing, drumPush, _sr );
// Size to the structure plus a ring-out tail. The tail grows with the ritard — it is a
// fixed number of SECONDS at the song's nominal tempo, and by the last bar the song is
// running slower than that, so a constant tail would be outrun by its own ending.
int total = _time.TotalSamples + (int)(_sr * RingOutTail * (1f + RitardAmount));
_bufL = new float[total];
_bufR = new float[total];
for ( int si = 0; si < structure.Count; si++ )
RenderSection( structure[si], si, _sectionStart[si], beatsPerBar,
si + 1 < structure.Count ? structure[si + 1] : structure[si] );
}
/// <summary>How much slower the song's final bars run than its nominal tempo.</summary>
const double RitardAmount = 0.22;
/// <summary>Which chord of the progression bar <paramref name="bar"/> of a section sits on.
/// One definition, because the tune has to be able to ask the same question — a melody drawn
/// against the changes only stays consonant if it is sung over the changes it was drawn for.
/// </summary>
internal int ChordIndexAt( in Part part, int bar )
=> (bar / _chordBars) % _prog.Length;
/// <summary>Length of a section in ticks, honouring any anomalous (short) bars.</summary>
internal static int SectionTicks( in Part p, int beatsPerBar )
{
int ticks = 0;
for ( int bar = 0; bar < p.Bars; bar++ )
ticks += BarBeats( p, bar, beatsPerBar ) * Timing.TicksPerBeat;
return ticks;
}
/// <summary>Beats in one bar of a section. Normally the song's meter; a section may name a
/// short bar (Biamonte's "anomalous measure" — a 2/4 inside a 4/4 context), which is how a
/// transition can cut a beat rather than politely filling the bar.</summary>
static int BarBeats( in Part p, int bar, int beatsPerBar )
=> p.BarBeats != null && bar < p.BarBeats.Length ? Math.Max( 1, p.BarBeats[bar] ) : beatsPerBar;
/// <summary>Which section a tick falls in.</summary>
int SectionAt( int tick, int count )
{
for ( int i = count - 1; i >= 0; i-- )
if ( tick >= _sectionStart[i] ) return i;
return 0;
}
/// <summary>Draw a figure from a table that may not exist for this genre. The draw is taken
/// either way — see the draw-count rule on <see cref="ComposePlan"/>.</summary>
static Pattern PickOrNull( Rng rng, Pattern[] table )
{
int i = rng.Int( Math.Max( 1, table?.Length ?? 1 ) );
return table == null || table.Length == 0 ? null : table[Math.Min( i, table.Length - 1 )];
}
/// <summary>The horn section's figure: a TWO-BAR call and response. The mask used to be eight
/// slots reused for the whole song, so the section played the identical stab pattern in every
/// bar of every chorus; bar 2 answering bar 1 is the actual ska convention, and it is what a
/// pattern with its own length buys.</summary>
Pattern HornFigure( Rng rng, int beatsPerBar )
{
int per = beatsPerBar * Timing.TicksPerBeat / Timing.TicksPerEighth;
var call = new int[per];
call[0] = CompFigure.Stab;
for ( int e = 1; e < per; e++ )
call[e] = rng.Chance( _c.HornDensity * (e % 2 == 1 ? 1.3f : 0.5f) )
? CompFigure.Stab : Harmony.Rest;
// The answer is the call displaced by a beat and opened up: it lands where the call left
// space, and pushes into the next bar on the last eighth.
var cells = new int[per * 2];
for ( int e = 0; e < per; e++ )
{
cells[e] = call[e];
cells[per + e] = call[(e + 2) % per];
}
cells[per] = Harmony.Rest; // the answer holds off the downbeat…
cells[per * 2 - 1] = CompFigure.Stab; // …and pushes into the next call
return Pattern.Eighths( cells );
}
// Render one section. Each voice gets its own per-section RNG stream keyed so that repeats
// of a section type reproduce identical backing, while the lead key folds in the verse
// index (so the Nth verse's lead differs) and the fill key folds in the absolute section
// index (so every section closes with a unique fill).
void RenderSection( Part part, int absIndex, int sectionTick, int beatsPerBar, Part next )
{
string bk = SectionKey( part.Type );
string lk = part.Type == Section.Verse ? $"verse:{part.VerseIndex}" : bk;
var bassRng = new Rng( $"{_tag}:bass:{bk}" );
var bassOrn = new Rng( $"{_tag}:bassorn:{bk}" );
var rhythmRng = new Rng( $"{_tag}:rhythm:{bk}" );
var keysRng = new Rng( $"{_tag}:keys:{bk}" );
var hornRng = new Rng( $"{_tag}:horn:{bk}" );
var leadRng = new Rng( $"{_tag}:lead:{lk}" );
// Expression (vibrato/bend/glide/scoop) rolls off their own stream so adding them
// leaves every voice's existing note CHOICES untouched — only pitch-shaping is layered on.
var exprRng = new Rng( $"{_tag}:expr:{lk}" );
var noise = new Rng( $"{_tag}:drums:{bk}" );
// Hats vs ride is decided per section off its own stream (keyed by section TYPE, so every
// chorus rides-or-hats the same, but a verse can differ).
_ride = new Rng( $"{_tag}:ride:{bk}" ).Chance( _ridePref );
var fillRng = new Rng( $"{_tag}:fill:{absIndex}" );
var fillNoise = new Rng( $"{_tag}:fillnoise:{absIndex}" );
// ── the section's figures ──
// The comp, keys and bass figures were drawn ONCE PER SONG, so a two-bar figure really was
// everything a listener ever heard — the backing read as one cell repeated for three
// minutes. The chorus keeps the song's own figure (that is the song's identity, and every
// chorus must agree); the other sections draw their own off a stream keyed by section TYPE,
// so a verse contrasts with the chorus while both verses still match each other.
//
// THE GROOVE IS ONE OF THEM NOW. It was the last draw in the engine that happened once per
// song and never again — the kit's two or three table entries were a genre's whole drumming,
// and one of them was a whole SONG's. Its own stream, so a section acquiring a groove of its
// own moves nothing else in the kit.
if ( part.Type != Section.Chorus )
{
var figRng = new Rng( $"{_tag}:figure:{bk}" );
_compFig = figRng.Pick( _prof.CompFigures );
_keysFig = PickOrNull( figRng, _prof.KeysFigures );
_bassPat = figRng.Pick( _prof.BassPatterns );
_groove = _prof.DrawGroove( new Rng( $"{_tag}:groove:{bk}" ) );
}
else
{
_compFig = _songComp; _keysFig = _songKeys; _bassPat = _songBass;
_groove = _songGroove;
}
_kickFig = _groove.Kick; _snareFig = _groove.Snare;
var tune = TuneFor( part.Type );
// ── the section's own state ──
// Everything below here reads these rather than asking "am I in a verse?": the energy
// contour, the half/double-time feel, and the key.
_sectionTick = sectionTick;
_sectionTicks = SectionTicks( part, beatsPerBar );
_energy = Math.Clamp( part.Energy, 0f, 1f );
_feel = part.Feel;
_keyShift = part.KeyShift;
_sectionType = part.Type;
// A CRASH-RIDE IS A TECHNIQUE, not a third cymbal: at the top of a genre's dynamic the
// cymbal hand moves off the ride and onto a crash, and the whole song lifts because the
// pulse is now being played on the loudest thing in the kit. It rides on top of the ride
// roll, so a section that was not riding at all does not suddenly acquire a cymbal, and
// it is a threshold on ENERGY rather than on section type, exactly like LoudComp.
_crashRide = _ride && _energy >= _prof.CrashRideFrom;
// And the foot's part under it (see FootOccupancy). Its own stream, so a section acquiring
// a pedal figure moves nothing else in the kit, and drawn per section because that is the
// scale a drummer holds a figure over.
_sections.Add( new SectionInfo( _time.TickToSample( sectionTick ),
_time.TickToSample( sectionTick + _sectionTicks ), _ride, _crashRide, $"{part.Type}",
_groove.Name ) );
var footRng = new Rng( $"{_tag}:foot:{bk}" );
_footCells = 0;
for ( int i = 0; i < 8; i++ )
if ( footRng.Chance( FootOccupancy[i] ) ) _footCells |= 1 << i;
// ── the arrangement ──
// One authority writes every part of this section against one skeleton, in one pass, off
// its own stream (so the song stream's draw count is untouched and its rule holds
// unchanged). Everything it needs exists by now: the section's state is published, the
// figures are drawn, the groove is a set of Patterns and the tune is a Pattern.
//
// It replaces _bassPat / _compFig / _keysFig with ARRANGED versions of themselves, which is
// why nothing downstream changed: a voice still slices the figure it was handed. Rewriting
// each voice to read the skeleton directly would have put the arranger's rules in six
// places and left every one of them able to disagree about what the section is — the same
// argument PlanTrace makes about re-deriving the plan.
PlanArrangement( part, sectionTick, beatsPerBar * Timing.TicksPerBeat, tune, bk );
// Recorded AFTER the arrangement, so what a sweep reads is the part the section will play
// rather than the table entry it started from.
Trace?.Mark( sectionTick, _sectionTicks, beatsPerBar * Timing.TicksPerBeat, part.Type,
_groove.Name, _kickFig, _snareFig, _groove.Cymbal );
bool isIntro = part.Type == Section.Intro;
bool isEnding = part.Type == Section.Ending;
// A fill that runs a whole bar or two has to be going somewhere: into a chorus, or out of
// a breakdown. Everywhere else it stays a beat or two.
bool bigBoundary = next.Type == Section.Chorus || part.Type == Section.Breakdown
|| part.Type == Section.PreChorus;
// ── the section's bar layout and its closing fill ──
// Bars are laid out first because a fill may be longer than the bar it lands in: it is
// planned once, in ticks, and the KIT stops where it begins. The melodic voices play
// through it, the way a band does — only the drums hand over.
var barStart = new int[part.Bars];
var barLen = new int[part.Bars];
int cursor = sectionTick;
for ( int bar = 0; bar < part.Bars; bar++ )
{
barLen[bar] = BarBeats( part, bar, beatsPerBar ) * Timing.TicksPerBeat;
barStart[bar] = cursor;
cursor += barLen[bar];
}
// The ending's fill moves one bar earlier so it sets up the final hit instead of pushing
// past the end into nothing.
int fillBar = isEnding ? part.Bars - 2 : part.Bars - 1;
int fillFrom = int.MaxValue, fillTo = 0;
if ( fillBar >= 0 && fillRng.Chance( _c.FillChance ) )
{
fillTo = barStart[fillBar] + barLen[fillBar];
fillFrom = Math.Max( sectionTick, FillStart( barStart[fillBar], barLen[fillBar], bigBoundary, fillRng ) );
}
for ( int bar = 0; bar < part.Bars; bar++ )
{
int barTick = barStart[bar];
int barTicks = barLen[bar];
_barTick = barTick;
// Harmonic rhythm is the genre's (GenreProfile.ChordBars): 2 bars/chord is the
// reggae-rock norm, while punk and pop take 1 so the four-chord loop IS the four-bar
// hypermeasure.
//
// nextChord is the NEXT BAR's chord, not the next slot's: with 2 bars/chord the first
// of the pair does not change harmony, and a bass approach note walking into a chord
// that is still a bar away just lands wrong.
int chord = ChordIndexAt( part, bar );
int nextChord = ChordIndexAt( part, bar + 1 );
// Where a suspension lands: half way through THIS CHORD's span, whatever the genre's
// harmonic rhythm is. At 2 bars/chord that is the second bar; at pop's 1 it is the
// second half of the bar, which is why this is a tick and not a bar index — a sus that
// only ever resolved on a bar line could never resolve at all where the chord IS a bar.
int chordBar0 = bar / _chordBars * _chordBars;
int chordTicks = 0;
for ( int b = chordBar0; b < part.Bars && b < chordBar0 + _chordBars; b++ )
chordTicks += barLen[b];
_susResolveTick = barStart[chordBar0] + chordTicks / 2;
// The ending lands on a held tonic chord that rings out — the band stops on the
// "one", it doesn't roll forward as if looping. The bar before it fills to lead in.
if ( isEnding && bar == part.Bars - 1 )
{
// The final chord is led out of the chord that was actually sounding before it
// (see EndingChord), so the ending has to be told what that was. Read at the last
// tick of the previous bar, which is past any suspension's landing — the same
// resolved spelling the ending itself uses.
_endingPrev = bar > 0
? ChordMidis( EndingBase(), ChordIndexAt( part, bar - 1 ), barTick - 1 )
: null;
RenderEnding( barTick, barTicks, noise );
break;
}
// The cadential regrouping: over a hemiola section's last two bars the chordal voice
// swaps its figure for one whose length does not divide the bar, so the comp and the
// bar line pull apart on the way into the next section.
bool hemiola = part.Hemiola && bar >= part.Bars - 2;
// Intro build-in: rather than slamming in at full band, the voices enter a layer at a
// time — bass + drums first, then the chordal voice, then the horns/lead on top. The
// thresholds are derived from the intro length so the build always spans it.
bool playChord = !isIntro || bar >= part.Bars / 4;
bool playTop = (!isIntro || bar >= part.Bars / 2) && _energy > 0.32f;
// RENDER ORDER. Where the bass follows the riff it has to know what the riff played,
// so the chordal voice goes first and the bass reads its onsets. Everywhere else the
// bass leads, as it always has.
_riffOnsets.Clear();
if ( _riffBass && playChord )
{
RenderComp( barTick, barTicks, chord, rhythmRng, keysRng, exprRng, hemiola );
RenderBassBar( barTick, barTicks, chord, nextChord, bassRng, bassOrn, exprRng );
}
else
{
RenderBassBar( barTick, barTicks, chord, nextChord, bassRng, bassOrn, exprRng );
if ( playChord )
RenderComp( barTick, barTicks, chord, rhythmRng, keysRng, exprRng, hemiola );
}
if ( _hornLead && _hasHorns && playTop )
RenderHornStabs( barTick, barTicks, chord, hornRng, exprRng );
RenderDrumBar( barTick, barTicks, fillFrom, noise );
// The melody. Where the section has a tune it SINGS it — the same one every time that
// section comes round, which is what makes a chorus a chorus. Sections without one (a
// solo, and metal's verses) are where the genre's lead grammar improvises instead.
if ( playTop && !isEnding )
{
if ( tune != null ) RenderTune( tune, barTick, barTicks, chord, leadRng, exprRng );
else if ( bar % Math.Max( 1, _prof.LeadPhraseBars ) == 0 )
RenderLeadPhrase( barTick, barTicks, chord, leadRng, exprRng );
}
}
// The fill, once, wherever it started — a beat, or the two bars before a final chorus.
if ( fillTo > 0 ) RenderFill( fillFrom, fillTo, fillNoise, fillRng );
}
/// <summary>The chordal layer: the genre's main comp figure, plus its keys/piano/synth voice
/// where it has one. Which of them plays what is <see cref="CompStyle"/>'s business.</summary>
void RenderComp( int barTick, int barTicks, int chord, Rng rhythmRng, Rng keysRng, Rng exprRng,
bool hemiola )
{
int to = barTick + barTicks;
// A loud section changes the TECHNIQUE, not just the level: the genre's loud figure through
// its loud style (third-wave ska's clean skank becoming driven power chords). The hemiola
// still wins where a section regroups — that is a metric device and it outranks the timbre.
bool loud = _songLoud != null && _energy >= _prof.LoudFrom;
// ── the flourish ──
// A player throws a flick in SOMETIMES. It used to be one entry in the genre's figure
// table, and figures are drawn per section — so a song that drew it played the flourish
// every two bars for a whole chorus, on a schedule, and a song that did not never heard
// the genre's signature gesture. Neither is an ornament; the first is a loop and the second
// is a coin toss. So it is rolled per OCCURRENCE, over the two-bar window the flourish
// figures are written on, which is the way the lead's ornaments have always worked.
//
// The roll happens for every genre on every window, ornament or not, so carrying one costs
// no extra values out of the composition stream (the PickOrNull discipline).
if ( (barTick - _sectionTick) % (barTicks * 2) == 0 )
{
_compOrn = rhythmRng.Chance( OrnamentChance ) && _prof.CompOrnament != null;
_keysOrn = keysRng.Chance( OrnamentChance ) && _prof.KeysOrnament != null;
}
// The hemiola outranks it, the same way it outranks the loud figure: a metric device beats
// a gesture. So does the loud technique — a third-wave chorus is a different part, not the
// skank with a flick on it.
bool plain = !hemiola && !loud;
var fig = hemiola ? CompFigure.Hemiola
: loud ? _songLoud
: _compOrn ? _prof.CompOrnament : _compFig;
RenderCompVoice( barTick, to, chord, fig, rhythmRng, exprRng, loud );
if ( _prof.Keys != KeysStyle.None && _keysFig != null && _energy > 0.35f )
RenderKeysVoice( barTick, to, chord, keysRng, exprRng, plain && _keysOrn );
}
// ── the ending ──
// The song's last bar, played by the band that played the rest of it. This used to be a fixed
// pad — one oscillator, one envelope, one length, the same in every genre and every song — so
// however different two songs were, they ended identically. It is the genre's own comp voice,
// its own bass and its own kit now, and WHICH ending is a per-song draw (see EndingStyle).
void RenderEnding( int barTick, int barTicks, Rng noise )
{
int at = _time.TickToSample( barTick );
int beat = Timing.TicksPerBeat;
double tail = RingOutTail * 0.92;
switch ( _ending )
{
case EndingStyle.StopHit:
// Everything at once, short, then nothing. A punk song does not ring out.
RenderKick( at, noise );
RenderCrashCym( at, _c.CrashVol * KitGain( barTick, 1f, 0.5f ), _crashBright, false );
EndingChord( barTick, beat / 2, 0.35, 1f );
EmitBass( at, _time.SpanSamples( barTick, beat / 2 ), ChordRoot( 0 ), 0.3,
NoteGain( 1f ), default );
break;
case EndingStyle.Cadence:
// V, then home on beat 3 — an actual cadence rather than a single held chord.
EndingChord( barTick, beat, 0.5, 0.8f, rootDegree: 4 );
EmitBass( at, _time.SpanSamples( barTick, beat ), ChordRoot4(), 0.45,
NoteGain( 0.85f ), default );
RenderKick( at, noise );
int land = barTick + 2 * beat;
RenderKick( _time.TickToSample( land ), noise );
RenderCrashCym( _time.TickToSample( land ), _c.CrashVol * KitGain( land, 1f, 0.5f ),
_crashBright, false );
EndingChord( land, (int)(_sr * tail), tail, 1f, samples: true );
EmitBass( _time.TickToSample( land ), (int)(_sr * tail), ChordRoot( 0 ), tail,
NoteGain( 1f ), default );
break;
case EndingStyle.Fall:
// The figure keeps going and falls away — four hits, each quieter, no final crash.
for ( int i = 0; i < 4; i++ )
{
int t = barTick + i * beat;
float v = 1f - i * 0.22f;
EndingChord( t, beat, 0.4, v );
if ( i % 2 == 0 ) RenderKick( _time.TickToSample( t ), noise );
EmitBass( _time.TickToSample( t ), _time.SpanSamples( t, beat ), ChordRoot( 0 ),
0.4, NoteGain( v ), default );
}
break;
default: // Ring — the band hits the tonic together and lets it decay into the tail.
RenderKick( at, noise );
EndingChord( barTick, (int)(_sr * tail), tail, 1f, samples: true );
EmitBass( at, (int)(_sr * tail), ChordRoot( 0 ), tail * 1.1, NoteGain( 1f ), default );
break;
}
}
/// <summary>The fifth of the key — the chord a cadence leans on before it lands.</summary>
int ChordRoot4() => Harmony.ScaleMidi( _rootMidi + _keyShift, _scale, 4 );
/// <summary>The final chord, voiced and TIMBRED as the genre's own chordal voice: a ska song
/// ends on horns, a metal song on the riff guitar, a pop song on the synth. The song's own
/// voicing carries too, so a ska ending lands on its 9th and a metal one on a bare fifth.
/// </summary>
/// <param name="rootDegree">Scale degree the chord is built on, or −1 for the song's HOME
/// chord — which is the progression's first slot, so the band lands where the bass lands
/// (RenderEnding plays ChordRoot(0) under it). The degree used to be folded in as
/// <c>_prog[0] + d + shift</c> on top of a <c>ChordDegrees</c> that had already added
/// <c>_prog[0]</c>: harmless while every progression started on the tonic, and a chord built a
/// third or a sixth off home for the pop and punk loops that do not.</param>
/// <summary>The register the ending's chord is voiced in — the genre's own chordal voice, so a
/// ska song lands on its horn section and a metal one on its guitar.</summary>
int EndingBase() => Register( _prof.Comp switch
{
CompStyle.Skank => 2, // ska: the horn section
CompStyle.Pad => 2, // pop: the synth
_ => 1, // the guitar genres
} );
void EndingChord( int tick, int dur, double decay, float vel, int rootDegree = -1,
bool samples = false )
{
int at = _time.TickToSample( tick );
int durSamples = samples ? dur : _time.SpanSamples( tick, dur );
bool ring = decay > 0.4;
int root = rootDegree < 0 ? _prog[0] : rootDegree;
// The register is the genre's own chordal voice; a driven guitar drops its third here too
// (see GuitarMidis) — a song must not land on the one chord it spent three minutes avoiding.
int chordBase = EndingBase();
// A song lands on a chord that STATES its quality, so the final chord is always the resolved
// spelling — a suspension is a thing owed, and the ending is where it is paid.
//
// AND IT IS VOICE-LED, like every other change. Building it in root position was deliberate
// once — a song should land where its genre voices the chord rather than where the last
// change left the register — but it made the ending the ONE unled change in the song and put
// it on the most exposed moment there is, so the last chord could leap a seventh out of the
// one before it. A cadence is a change; the register the band has held for three minutes is
// where it lands; and the ritard is not cover for a jump. The genre's own voicing and its
// own register still decide the chord — this only picks the inversion, inside the same one
// octave every other change gets (Harmony.LeadToward). _endingPrev is what was sounding
// before it, which is null for the first chord of the ending, and a null simply leaves root
// position where there is nothing to lead from.
var tones = VoicedMidis( chordBase, root, _voicingRes );
var lead = Harmony.LeadToward( _scale, _endingPrev, chordBase, root, _voicingRes );
for ( int i = 0; i < tones.Length; i++ ) tones[i] += lead[i];
_endingPrev = CopyOf( tones );
if ( _prof.Comp is not (CompStyle.Skank or CompStyle.Pad) )
tones = DrivenVoicing( tones, _voicingRes );
foreach ( var midi in tones )
{
Patch p;
switch ( _prof.Comp )
{
case CompStyle.Skank: // ska: the horn section holds the last chord
p = new Patch
{
Osc = 1, Voices = 3, Detune = _c.Detune,
Amp = _c.HornVol * _c.HornBalance * _midMul / tones.Length * NoteGain( vel ),
Attack = 0.01f, Decay = decay, Sustain = ring ? 0.35f : 0f, Sustained = false,
Cutoff = _c.HornCutoff, CutEnv = 1200f, Reso = 1.0f, Drive = _c.HornDrive,
Pan = 0f, Vibrato = _c.MelodyVibrato,
};
break;
case CompStyle.Pad: // pop: the synth
p = new Patch
{
Osc = 1, Voices = 2, Detune = _c.Detune * 0.5f,
Amp = _c.KeysVol * _c.KeysBalance * KeysLevel() * _midMul / tones.Length
* NoteGain( vel ),
Attack = 0.004f, Decay = decay, Sustain = ring ? 0.5f : 0f, Sustained = false,
Cutoff = _c.KeysCutoff, CutEnv = 250f, Reso = 1.0f, Drive = KeysDriveFor(),
Pan = 0f,
};
break;
default: // the guitar genres, through their own tone
var (drive, cutEnv, reso, level) = RhythmGtrTone();
p = new Patch
{
Osc = 1, Voices = 2, Detune = _c.Detune * 0.5f,
Amp = _c.RhythmGtrVol * _c.RhythmGtrBalance * level * _midMul / tones.Length
* NoteGain( vel ),
Attack = 0.002f, Decay = decay, Sustain = ring ? 0.4f : 0f, Sustained = false,
Cutoff = _c.RhythmGtrCutoff, CutEnv = cutEnv, Reso = reso, Drive = drive,
Pan = 0f,
};
break;
}
// Pop's ending is the keys voice, and the keys are not double-tracked (see EmitKeys).
RenderPatch( at, durSamples, Midi( midi ), p, mono: _prof.Comp == CompStyle.Pad );
}
}
// ── dynamics ──
// Velocity as a first-class value: the pattern cell's own weight times the section's energy.
// Every voice scales its level through here rather than inventing its own — a per-patch
// constant with two ad-hoc exceptions was the flat, mechanical tell that survived every
// rhythmic fix.
//
// THERE IS NO TICK HERE, AND THAT IS THE POINT. This used to multiply in MetricGain, so a
// pitched note's level was decided by WHERE IN THE BAR IT LANDED — and the weights MetricGain
// returns were measured off drum hits. A melody is drawn on the eighth grid, so it alternates
// on-beat and off-beat constantly and its level stepped with it: 3 dB a note in rock and 5 dB
// in pop, out of metric position alone. That is a drummer's dynamic worn by a singer, and it
// reads as strange as it is. A DATASET OF DRUM VELOCITIES CAN SAY WHAT A DRUMMER DOES AND
// CANNOT SAY WHAT THE BAND DOES — GenreProfile's accent block already said so as a caveat, and
// it is the rule now. Taking the parameter away is what makes it one: a pitched voice cannot
// ask for a metric accent, the same way Register makes a non-octave base unwriteable.
//
// A phrase-shaped dynamic for the melody is a different thing and a real one — it would come
// off the TUNE rather than off the grid, so it belongs in Melody, and it is a PLAN row.
float NoteGain( float vel ) => vel * EnergyGain( 0.35f );
/// <summary>The kit's gain: the genre's accent weight for where <paramref name="tick"/> falls
/// in the bar, times the cell's own velocity and the section's energy. The accent weights were
/// measured off drums and this is the only door out of MetricGain — see NoteGain.</summary>
/// <param name="depth">how much this drum voice cares about the section's energy.</param>
float KitGain( int tick, float vel, float depth ) => vel * MetricGain( tick ) * EnergyGain( depth );
/// <summary>The genre's accent weight for where <paramref name="tick"/> falls in the bar.</summary>
float MetricGain( int tick )
{
int bar = _time.BarTicks;
int rel = ((tick - _barTick) % bar + bar) % bar;
if ( rel == 0 ) return _prof.AccentDown;
if ( rel % Timing.TicksPerBeat != 0 ) return _prof.AccentOff;
return (rel / Timing.TicksPerBeat) % 2 == 1 ? _prof.AccentBack : 1f;
}
/// <summary>Section energy as a gain. <paramref name="depth"/> is how much this voice cares:
/// 0 = plays at full level in a breakdown, 1 = disappears entirely.</summary>
float EnergyGain( float depth ) => 1f - depth * (1f - _energy);
/// <summary>A genre mix trim, scaled by the runtime GENRE MIX amount so the whole per-genre
/// mix can be dialled back (or off) from skafinity.config.json without a rebuild.</summary>
float MixTrim( float trim ) => 1f + (trim - 1f) * Math.Clamp( _c.GenreMix, 0f, 2f );
}