Part of the MusicGen engine, this file implements the composition pass and section rendering for procedural song generation. It plans song-wide parameters, builds section/timing structures, draws per-section RNG streams, arranges figures, and renders comp, bass, drums, leads and endings into sample buffers.
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 );
}