Code/Engine/Arrange.cs

Arranger code for the MusicGen system. Defines rhythmic skeletons (sixteenth grid, bar seams, kick/tune occupancy) and routines to arrange kit and band parts by mutating authored patterns (quote, drop, add, displace, recombine) to fit the section's accent grid and constraints.

Native Interop
using System;
using System.Collections.Generic;

namespace Skafinity;

/// <summary>Which cells of a bar a voice is ALLOWED to play on. This is what keeps ska's skank
/// offbeat by RULE rather than by table — the arranger may move an onset, but not off the class
/// the genre's technique lives in, so a skank cannot drift onto the downbeat however loud the
/// accent grid is there.
///
/// It is the single property that has to survive the whole arranger, and it is the reason the
/// arranger cannot simply write onsets wherever the accent grid is loud. A genre's identity is
/// WHERE it plays; its arrangement is which of those places it uses this time.</summary>
enum CellClass
{
	/// <summary>Every sixteenth — metal's gallop, and anything that subdivides freely.</summary>
	Sixteenths,
	/// <summary>Every eighth — the rock riff, the pop pad.</summary>
	Eighths,
	/// <summary>The beats only — punk's downstrokes.</summary>
	Downbeats,
	/// <summary>The "and" of each beat only — the ska skank and country's chick.</summary>
	Offbeats,
}

/// <summary>
/// THE SECTION'S RHYTHMIC SKELETON — what every part is written against.
///
/// Before this, every voice picked its figure from its own small authored table and no voice knew
/// what any other was playing. The single exception was the riff-doubling bass, and it was the
/// only lockup in the engine that was not a coincidence: pop's pad landed on the kick 100% of the
/// time and ska's skank 1%, and neither number was decided by anyone.
///
/// The skeleton is deliberately DERIVED, not drawn. The kit is table-driven and its grooves are
/// fitted to a played corpus (see <see cref="DrumGroove"/>), so the drums are the measured
/// reference the arranger writes against rather than another client of it — which is why the
/// accent grid comes off the kick and the snare and the genre's own measured accent weights, and
/// why nothing here rolls a die to decide where the section leans.
///
/// Everything is on the section's own SIXTEENTH grid. Occupancy fills in as each part is placed,
/// so a voice arranged later can see what the ones before it took — that is what "one authority
/// arranges every part at once" amounts to in practice.
/// </summary>
sealed class Skeleton
{
	public const int CellTicks = Timing.TicksPerEighth / 2;

	/// <summary>First tick of the section, and how many sixteenth cells long it is.</summary>
	public readonly int StartTick, Cells;

	/// <summary>Cells per bar — the seam and allowed-class tests are per bar.</summary>
	public readonly int BarCells;

	/// <summary>Where the section leans, 0..1, off the groove's kick and snare and the genre's
	/// measured accent weights.</summary>
	public readonly float[] Accent;

	/// <summary>Where the kick lands. The bass's lock reads this directly rather than the accent
	/// grid — "agrees with the kick" is a different claim from "is loud in the same place".</summary>
	public readonly bool[] Kick;

	/// <summary>Phrase ends: every four bars, and the section's last bar. Where a band converges.
	/// </summary>
	public readonly bool[] Seam;

	/// <summary>Where the tune has an onset, and where it is holding a note through.</summary>
	public readonly bool[] TuneOn, TuneHold;

	/// <summary>What the parts placed so far have taken. Mutated as the arranger works down the
	/// voices, which is the whole point of arranging them in one pass.</summary>
	public readonly bool[] Taken;

	public Skeleton( int startTick, int ticks, int barTicks )
	{
		StartTick = startTick;
		Cells = Math.Max( 1, ticks / CellTicks );
		BarCells = Math.Max( 1, barTicks / CellTicks );
		Accent = new float[Cells];
		Kick = new bool[Cells];
		Seam = new bool[Cells];
		TuneOn = new bool[Cells];
		TuneHold = new bool[Cells];
		Taken = new bool[Cells];
	}

	/// <summary>The cell a SONG tick falls in, or −1 outside the section.</summary>
	public int CellAt( int tick )
	{
		int c = (tick - StartTick) / CellTicks;
		return c < 0 || c >= Cells ? -1 : c;
	}

	/// <summary>Whether a cell is one this voice's technique may play on.</summary>
	public bool Allows( CellClass cls, int cell )
	{
		int inBar = cell % BarCells;
		return cls switch
		{
			CellClass.Sixteenths => true,
			CellClass.Eighths => inBar % 2 == 0,
			CellClass.Downbeats => inBar % 4 == 0,
			_ => inBar % 4 == 2,
		};
	}
}

/// <summary>How a voice arranges itself against the skeleton: where it may play, and what it is
/// pulled toward or pushed away from. The parameters are the genre's (see
/// <see cref="GenreProfile"/>) because they say HOW a part behaves, never WHAT it plays — the
/// figure is still the genre's own authored gesture.</summary>
readonly struct ArrangeRole
{
	public readonly CellClass Cells;
	/// <summary>Pull toward cells the kick plays — how hard this voice locks to the drums.</summary>
	public readonly float Kick;
	/// <summary>Push away from cells another part has already taken, and from the tune's landings.
	/// </summary>
	public readonly float Complement;
	/// <summary>Pull toward phrase seams, where a band converges.</summary>
	public readonly float Seam;

	/// <summary>What an ADDED onset plays, where the voice's vocabulary says an addition is one
	/// particular thing rather than "another of whatever was before it". The snare is the case: a
	/// drummer filling in around a backbeat adds GHOSTS, and copying the previous cell would let a
	/// bar acquire a second struck backbeat — which is the one thing about a snare part a listener
	/// would notice immediately. <see cref="NoValue"/> keeps the copy-the-previous default.
	/// </summary>
	public readonly int AddValue;

	public const int NoValue = int.MinValue;

	public ArrangeRole( CellClass cells, float kick, float complement, float seam,
		int addValue = NoValue )
	{ Cells = cells; Kick = kick; Complement = complement; Seam = seam; AddValue = addValue; }
}

// The arranger. Part of the MusicGen engine — see MusicGen.cs.

public sealed partial class MusicGen
{
	/// <summary>The current section's skeleton, published in <c>RenderSection</c> alongside
	/// <c>_energy</c> / <c>_feel</c> / <c>_keyShift</c> — the same mechanism, so a voice reads it
	/// the way it reads those.</summary>
	Skeleton _skeleton;

	/// <summary>Build the section's skeleton, then arrange each part against it in turn.
	///
	/// ORDER IS THE DESIGN: the bass goes first because its role is to agree with the kick, which
	/// is already decided; the comp then sees the bass and the tune and can complement them; the
	/// keys see all three. Arranging them independently against a fixed grid would give every voice
	/// the same answer, which is the failure mode this whole phase has to avoid.</summary>
	void PlanArrangement( in Part part, int sectionTick, int barTicks, Pattern tune, string bk )
	{
		var sk = new Skeleton( sectionTick, _sectionTicks, barTicks );
		_skeleton = sk;
		_kitArranged = false;

		// ── the seams ──
		// A phrase ends every four bars, and the section's last bar is one whatever its length.
		for ( int bar = 4; bar * sk.BarCells < sk.Cells; bar += 4 )
			sk.Seam[bar * sk.BarCells - 1] = true;
		if ( sk.Cells > 0 ) sk.Seam[sk.Cells - 1] = true;

		// ── the tune's occupancy ──
		// The tune is written first and everything else is written against it, which is the order a
		// song is actually made in. Note that the tune is exempt from the section's feel, so it is
		// sliced at the nominal rate here exactly as RenderTune slices it.
		if ( tune != null )
		{
			int anchor = _sectionTicks > 0 && _sectionTicks < tune.LengthTicks
				? sectionTick - (tune.LengthTicks - _sectionTicks) : sectionTick;
			foreach ( var h in tune.Slice( sectionTick, sectionTick + _sectionTicks, anchor ) )
			{
				int c = sk.CellAt( h.Tick );
				if ( c < 0 ) continue;
				sk.TuneOn[c] = true;
				int held = Math.Min( h.SpanTicks, Timing.TicksPerBeat * 2 ) / Skeleton.CellTicks;
				for ( int k = 1; k < held && c + k < sk.Cells; k++ ) sk.TuneHold[c + k] = true;
			}
		}

		// ── who goes first ──
		// The skeleton the band writes against is the kit's accents, plus the genre's metric
		// weights, plus the phrase seams, plus the tune. THREE OF THOSE FOUR DO NOT NEED THE KIT,
		// which is what makes both orderings one mechanism rather than two.
		//
		// KIT LEADS: the kit arranges against seams, metre and tune; its accents then go on the
		// grid; the band follows. KIT FOLLOWS: the band writes against a grid with no kit on it and
		// the kit arranges last, against what the band actually took — a drummer playing to the
		// riff. It is not a coin flip dressed up: a leading kit is most punk and most rock, a
		// following kit is riff-led metal and a great deal of programmed pop.
		var rng = new Rng( $"{_tag}:arr:{bk}" );
		if ( _kitLeads ) { ArrangeKit( sk, bk ); KitAccents( sk ); }
		else KitAccents( sk );

		ArrangeBand( part, sk, rng );

		if ( !_kitLeads ) { ArrangeKit( sk, bk ); KitAccents( sk ); }
	}

	/// <summary>The accent grid, off the kit and the genre's own measured weights.
	///
	/// WITH NO KIT ON THE GRID YET the cells carry the metre alone — which is the honest reading of
	/// "the band writes against seams, metre and tune", and not a floor invented to keep the number
	/// non-zero: it is exactly this formula with the kit's occupancy taken as flat.</summary>
	void KitAccents( Skeleton sk )
	{
		bool haveKit = _kitArranged;
		for ( int c = 0; c < sk.Cells; c++ ) { sk.Accent[c] = 0f; sk.Kick[c] = false; }

		if ( haveKit )
		{
			foreach ( var h in _kickFig.Slice( sk.StartTick, sk.StartTick + _sectionTicks, sk.StartTick, _feel ) )
			{
				int c = sk.CellAt( h.Tick );
				if ( c < 0 ) continue;
				sk.Kick[c] = true;
				sk.Accent[c] += h.Vel;
			}
			foreach ( var h in _snareFig.Slice( sk.StartTick, sk.StartTick + _sectionTicks, sk.StartTick, _feel ) )
			{
				int c = sk.CellAt( h.Tick );
				if ( c >= 0 ) sk.Accent[c] += h.Value == DrumGroove.Ghost ? 0.3f : h.Vel;
			}
		}

		// The genre's own measured accent weights on top: a country bar leans on its offbeat and a
		// metal bar is deliberately flat, and that is a property of the genre rather than of the
		// groove it drew.
		for ( int c = 0; c < sk.Cells; c++ )
		{
			int inBar = c % sk.BarCells;
			float metric = inBar == 0 ? _prof.AccentDown
				: inBar % 4 != 0 ? _prof.AccentOff
				: (inBar / 4) % 2 == 1 ? _prof.AccentBack : 1f;
			sk.Accent[c] = Math.Min( 1f, (haveKit ? sk.Accent[c] : 1f) * metric * 0.6f );
		}
	}

	void ArrangeBand( in Part part, Skeleton sk, Rng rng )
	{
		// ── the parts ──
		// EVERY CHORUS AGREES, AND THE CHORUS IS STILL ARRANGED. Those are two different claims and
		// conflating them is what would make this whole phase a no-op: if a chorus quoted the TABLE
		// rather than quoting the other choruses, the song's own rhythm section would stay one entry
		// out of a table of three, which is the ceiling this exists to break — and the choruses are
		// most of what a listener hears as the song.
		//
		// So the chorus is arranged ONCE and cached as the song's own part. Every later chorus
		// reuses the cached line rather than re-deriving it: the guarantee becomes structural
		// instead of resting on the skeleton happening to come out the same at three different
		// points in the song.
		if ( part.Type == Section.Chorus )
		{
			if ( !_chorusArranged )
			{
				_songBass = Arrange( _songBass, sk, rng, _prof.BassRole, _prof.BassPatterns );
				_songComp = Arrange( _songComp, sk, rng, _prof.CompRole, _prof.CompFigures );
				// The LOUD figure is the chorus part in any genre that changes technique when the
				// section is loud, so leaving it un-arranged would leave exactly the bars a listener
				// remembers coming straight out of a table of two.
				if ( _songLoud != null )
					_songLoud = Arrange( _songLoud, sk, rng, _prof.LoudCompRole ?? _prof.CompRole,
						_prof.LoudCompFigures );
				if ( _songKeys != null )
					_songKeys = Arrange( _songKeys, sk, rng, _prof.KeysRole, _prof.KeysFigures );
				_chorusArranged = true;
			}
			else { MarkTaken( sk, _songBass ); MarkTaken( sk, _songComp ); MarkTaken( sk, _songKeys ); }
			_bassPat = _songBass; _compFig = _songComp; _keysFig = _songKeys;
			return;
		}

		_bassPat = Arrange( _bassPat, sk, rng, _prof.BassRole, _prof.BassPatterns );
		_compFig = Arrange( _compFig, sk, rng, _prof.CompRole, _prof.CompFigures );
		if ( _keysFig != null )
			_keysFig = Arrange( _keysFig, sk, rng, _prof.KeysRole, _prof.KeysFigures );
	}

	/// <summary>Whether the song's chorus parts have been arranged yet. The chorus is arranged the
	/// first time one is rendered and every later chorus reuses that line.</summary>
	bool _chorusArranged, _chorusKitArranged;

	/// <summary>Whether the kit's patterns for THIS section are final yet — i.e. whether the accent
	/// grid may be built off them.</summary>
	bool _kitArranged;

	/// <summary>
	/// The kit, arranged. The drums were the one layer left out of the arranger, and the reasoning
	/// was good — the grooves are fitted to a played corpus, so they were the measured reference the
	/// band wrote against rather than another client of it. The cost was that the groove was drawn
	/// ONCE PER SONG and never re-drawn: every bar of every section played the identical kick, snare
	/// and cymbal, two or three states per genre and exactly one inside a song.
	///
	/// THE CYMBAL IS NOT ARRANGED. It is the pulse, and it is where the corpus pass found the
	/// largest mismatch of all (country's hat on the "and", 84% against 36% on the beat) — leaving
	/// it alone preserves that by construction rather than by a rule that can be got wrong. What
	/// varies about the cymbal is which INSTRUMENT plays it and how sparse a section thins it, both
	/// of which already vary per section.
	///
	/// EVERY CHORUS AGREES, the same way the band's does and for the same reason: the song's kit is
	/// arranged the first time a chorus is rendered and every later chorus replays that line.
	/// </summary>
	void ArrangeKit( Skeleton sk, string bk )
	{
		var rng = new Rng( $"{_tag}:kit:{bk}" );
		if ( _sectionType == Section.Chorus )
		{
			if ( !_chorusKitArranged )
			{
				_songKick = ArrangeDrum( _songKick, sk, rng, KickRole(), kick: true );
				_songSnare = ArrangeDrum( _songSnare, sk, rng, _prof.SnareRole, kick: false );
				_chorusKitArranged = true;
			}
			_kickFig = _songKick; _snareFig = _songSnare;
			_kitArranged = true;
			return;
		}

		_kickFig = ArrangeDrum( _kickFig, sk, rng, KickRole(), kick: true );
		_snareFig = ArrangeDrum( _snareFig, sk, rng, _prof.SnareRole, kick: false );
		_kitArranged = true;
	}

	/// <summary>
	/// Where a section is quiet enough that the kit plays the SPINE and nothing else — the
	/// downbeat kick and the struck backbeat, with every ghost and every pushed kick gone.
	///
	/// AND THE FEEL GATE IS THE POINT, not a caveat. Half time is a pattern RATE: it already
	/// stretches the groove to half its density, so a breakdown that also thinned to the spine
	/// would be two hits a bar and a hole in the arrangement. Every Breakdown in every form here
	/// is <c>feel: 0.5f</c>, which is exactly why this fires on the INTRO instead — a kit walking
	/// in on the bare bones of its own groove, which is what an intro is.
	/// </summary>
	const float KitSpineFrom = 0.32f;

	/// <summary>
	/// THE KIT'S DENSITY BIAS, −1 (thin) to +1 (fill), and what makes energy an input to what the
	/// drummer PLAYS rather than only to how loud it is.
	///
	/// It shifts the weight between the DROP and ADD mutations, so a quiet section is likelier to
	/// lose an onset and a loud one to gain one. Everything a drummer does with dynamics beyond
	/// hitting harder is here: fewer notes, dropped ghosts, a busier bar under a chorus.
	///
	/// <c>DrumBusy</c> and <c>DrumTone</c> feed the same decision rather than sitting on top of it
	/// as multipliers — that is the whole reason they are here. A bright kit is snare-led, so
	/// <c>DrumTone</c> pushes the ghost layer up and the foot down; a dark one does the reverse.
	/// </summary>
	float KitBias( bool kick )
	{
		float energy = (_energy - 0.5f) * 1.4f;
		float busy = (Math.Clamp( _c.DrumBusy, 0f, 1f ) - 0.5f) * 1.2f;
		float tone = (_drumTone - 0.5f) * 0.6f * (kick ? -1f : 1f);
		return Math.Clamp( energy + busy + tone, -1f, 1f );
	}

	/// <summary>The spine alone — see <see cref="KitSpineFrom"/>.</summary>
	static Pattern ToSpine( Pattern fig, bool[] spine )
	{
		int n = 0;
		for ( int i = 0; i < spine.Length; i++ ) if ( spine[i] ) n++;
		if ( n == 0 || n == fig.Count ) return fig;
		var ticks = new int[n]; var values = new int[n]; var vels = new float[n];
		for ( int i = 0, k = 0; i < fig.Count; i++ )
		{
			if ( !spine[i] ) continue;
			ticks[k] = fig.TickAt( i ); values[k] = fig.ValueAt( i ); vels[k] = fig.VelAt( i ); k++;
		}
		return new Pattern( fig.LengthTicks, ticks, values, vels );
	}

	/// <summary>
	/// The kick's role, WITH THE SIGN OF ITS COMPLEMENT DECIDED BY WHO WROTE FIRST — and that sign
	/// is the whole difference between the two orderings.
	///
	/// <see cref="Score"/> reads <c>Complement</c> as a push AWAY from cells the tune and the parts
	/// placed so far have taken, which is right for every melodic voice and right for a kick the
	/// band has not been written against yet: a leading kit states the beat and the band answers it.
	/// A FOLLOWING kick is the opposite gesture. The riff is already on the grid, and a drummer
	/// playing to it lands WITH it — that is what "the kick tracks the topline" means in programmed
	/// pop and what a riff-led metal foot is doing under a gallop.
	///
	/// Ordering with the same sign on both sides is what the split reporting caught: it changed who
	/// saw whom and left both modes agreeing to within a point or two, which is a mechanism that
	/// costs a draw and buys nothing.
	/// </summary>
	ArrangeRole KickRole()
	{
		var r = _prof.KickRole;
		return _kitLeads ? r
			: new ArrangeRole( r.Cells, r.Kick, -r.Complement, r.Seam, r.AddValue );
	}

	/// <summary>One drum, through the same mutations as everything else — at the kit's own lower
	/// rate, with the genre's spine held back, and without writing itself into the occupancy the
	/// band reads.
	///
	/// The RECOMBINE table is this drum's line from the genre's OTHER grooves, which is the same
	/// claim the melodic version makes: the genre's own vocabulary, re-cut, and never a gesture the
	/// genre does not have.</summary>
	Pattern ArrangeDrum( Pattern fig, Skeleton sk, Rng rng, in ArrangeRole role, bool kick )
	{
		if ( fig == null ) return null;
		var spine = DrumGroove.SpineOf( fig, kick, _time.BarTicks );
		var table = new Pattern[_prof.Grooves.Length];
		for ( int i = 0; i < table.Length; i++ )
			table[i] = kick ? _prof.Grooves[i].Kick : _prof.Grooves[i].Snare;
		// The arrangement happens either way, so the stream costs the same whatever the section's
		// energy — the spine section then keeps only what it was always going to keep.
		var arranged = Arrange( fig, sk, rng, role, table, _prof.KitMutateRate, spine,
			marks: false, bias: KitBias( kick ), offPulse: kick );
		if ( _energy > KitSpineFrom || _feel < 1f ) return arranged;
		return ToSpine( arranged, DrumGroove.SpineOf( arranged, kick, _time.BarTicks ) );
	}

	/// <summary>Write a figure's onsets into the skeleton's occupancy without changing it — what a
	/// quoted part still owes the parts arranged after it.</summary>
	void MarkTaken( Skeleton sk, Pattern fig )
	{
		if ( fig == null ) return;
		foreach ( var h in fig.Slice( sk.StartTick, sk.StartTick + _sectionTicks, sk.StartTick, _feel ) )
		{
			int c = sk.CellAt( h.Tick );
			if ( c >= 0 ) sk.Taken[c] = true;
		}
	}

	/// <summary>How often a non-chorus section plays its figure verbatim rather than working on
	/// it. The rest of the weight is shared over the four mutations below.</summary>
	const float QuoteWeight = 1f;

	/// <summary>
	/// Arrange one part: the genre's authored figure, worked on against the section's skeleton.
	///
	/// THE TABLES ARE SEED MATERIAL, NOT A CEILING. The authored figures are each genre's
	/// characteristic gestures and none of them is deleted — what changes is that a section's part
	/// is now figure x mutation x skeleton rather than one entry out of a table of three. That
	/// product is where the state count comes from: the whole rhythm section used to have twelve
	/// states in punk over five hundred songs, because it was the product of three table sizes and
	/// randomness cannot reach past a table size.
	///
	/// Every mutation stays inside the genre's allowed cell class, so a skank stays offbeat and a
	/// punk downstroke stays on the beat however loud the accent grid is elsewhere.
	/// </summary>
	Pattern Arrange( Pattern fig, Skeleton sk, Rng rng, in ArrangeRole role, Pattern[] table )
		=> Arrange( fig, sk, rng, role, table, _prof.MutateRate );

	/// <param name="spine">Onsets the mutations may not reach, index-aligned with
	/// <paramref name="fig"/> — the drums' <see cref="DrumGroove.SpineOf"/>. Null for a voice whose
	/// whole figure is fair game, which is every melodic one.</param>
	/// <param name="marks">Whether the result goes into the skeleton's occupancy. The kit's does
	/// not: the kick has its own layer on the grid and the snare is most of the accent grid, so
	/// writing it into <c>Taken</c> as well would have the band pushing away from a beat it is
	/// supposed to be locking to, and count it twice while doing it.</param>
	/// <param name="bias">−1 (thin) to +1 (fill): shifts weight between DROP and ADD without
	/// changing how much of the stream the draw costs. Zero for every melodic voice, so their
	/// weights are exactly what they always were; the kit reads its section's energy and the
	/// vibe's DRUM BUSY through it (see <see cref="KitBias"/>).</param>
	Pattern Arrange( Pattern fig, Skeleton sk, Rng rng, in ArrangeRole role, Pattern[] table,
		float mutateRate, bool[] spine = null, bool marks = true, float bias = 0f,
		bool offPulse = false )
	{
		if ( fig == null || fig.Count == 0 ) { return fig; }

		// The draw is taken whatever the outcome, so a genre's mutation rate cannot change how much
		// of this stream the next voice sees — the same discipline PickOrNull keeps in the composer.
		float mutate = Math.Clamp( mutateRate, 0f, 1f );
		int op = rng.WeightedIndex( new[]
		{
			(int)MathF.Round( QuoteWeight * (1f - mutate) * 100f ),   // quote
			(int)MathF.Round( mutate * 30f * (1f - bias) ),           // drop
			(int)MathF.Round( mutate * 30f * (1f + bias) ),           // add
			(int)MathF.Round( mutate * 25f ),                         // displace
			(int)MathF.Round( mutate * 15f ),                         // recombine
		} );

		var ticks = new List<int>();
		var values = new List<int>();
		var vels = new List<float>();
		for ( int i = 0; i < fig.Count; i++ )
		{ ticks.Add( fig.TickAt( i ) ); values.Add( fig.ValueAt( i ) ); vels.Add( fig.VelAt( i ) ); }

		switch ( op )
		{
			case 1: Drop( ticks, values, vels, fig, sk, rng, role, spine ); break;
			case 2: Add( ticks, values, vels, fig, sk, rng, role, offPulse ); break;
			case 3: Displace( ticks, values, vels, fig, sk, rng, role, spine, offPulse ); break;
			case 4: Recombine( ticks, values, vels, fig, rng, table, spine ); break;
		}

		var arranged = ticks.Count == 0 ? fig
			: new Pattern( fig.LengthTicks, ticks.ToArray(), values.ToArray(), vels.ToArray() );
		if ( marks ) MarkTaken( sk, arranged );
		return arranged;
	}

	// ── scoring ──
	// A figure loops inside the section, so a candidate position is judged over EVERY repetition it
	// will actually be played at rather than over the first one. A one-bar figure in an eight-bar
	// section is played eight times; scoring it against bar 1 alone would arrange it for a bar it
	// spends seven eighths of its life away from.
	float Score( int figTick, Pattern fig, Skeleton sk, in ArrangeRole role )
	{
		float sum = 0; int n = 0;
		for ( int rep = 0; ; rep++ )
		{
			int t = sk.StartTick + (int)Math.Round( (rep * (double)fig.LengthTicks + figTick) / Math.Max( 0.01f, _feel ) );
			int c = sk.CellAt( t );
			if ( c < 0 ) break;
			sum += sk.Accent[c]
				+ role.Kick * (sk.Kick[c] ? 1f : 0f)
				+ role.Seam * (sk.Seam[c] ? 1f : 0f)
				- role.Complement * ((sk.TuneOn[c] ? 1f : 0f) + (sk.Taken[c] ? 0.6f : 0f));
			n++;
		}
		return n == 0 ? float.NegativeInfinity : sum / n;
	}

	bool AllowedFigTick( int figTick, Pattern fig, Skeleton sk, in ArrangeRole role )
	{
		int c = sk.CellAt( sk.StartTick + (int)Math.Round( figTick / Math.Max( 0.01f, _feel ) ) );
		return c >= 0 && figTick % Skeleton.CellTicks == 0 && sk.Allows( role.Cells, c );
	}

	/// <summary>DROP the onset that fights hardest with what is already there — a tune landing the
	/// comp is stepping on, most often. Never the figure's first onset: a figure that loses its
	/// downbeat is a different figure.</summary>
	void Drop( List<int> ticks, List<int> values, List<float> vels, Pattern fig, Skeleton sk,
		Rng rng, in ArrangeRole role, bool[] spine = null )
	{
		if ( ticks.Count <= 2 ) return;
		int worst = -1; float worstScore = float.MaxValue;
		for ( int i = 1; i < ticks.Count; i++ )
		{
			if ( spine != null && spine[i] ) continue;
			float s = Score( ticks[i], fig, sk, role );
			if ( s < worstScore ) { worstScore = s; worst = i; }
		}
		if ( worst < 0 ) return;
		ticks.RemoveAt( worst ); values.RemoveAt( worst ); vels.RemoveAt( worst );
	}

	/// <summary>ADD an onset on the best free cell of the genre's allowed class. The new hit takes
	/// its VALUE from the onset before it, so it is the same gesture played once more rather than a
	/// cell type the figure never used.</summary>
	/// <param name="offPulse">Refuse cells on the beat — the other half of the kick's spine law
	/// (see <see cref="DrumGroove.SpineOf"/>). A groove's identity is partly where it does NOT
	/// play, and a rule about existing onsets cannot say that: the one drop IS the hole on beat 1,
	/// and without this it quietly acquires the downbeat it is defined by not having.</param>
	void Add( List<int> ticks, List<int> values, List<float> vels, Pattern fig, Skeleton sk,
		Rng rng, in ArrangeRole role, bool offPulse = false )
	{
		int best = -1; float bestScore = float.NegativeInfinity;
		for ( int t = 0; t < fig.LengthTicks; t += Skeleton.CellTicks )
		{
			if ( offPulse && DrumGroove.IsPulse( t ) ) continue;
			if ( ticks.Contains( t ) || !AllowedFigTick( t, fig, sk, role ) ) continue;
			float s = Score( t, fig, sk, role );
			if ( s > bestScore ) { bestScore = s; best = t; }
		}
		if ( best < 0 ) return;
		int at = 0;
		while ( at < ticks.Count && ticks[at] < best ) at++;
		int from = Math.Max( 0, at - 1 );
		ticks.Insert( at, best );
		values.Insert( at, role.AddValue == ArrangeRole.NoValue ? values[from] : role.AddValue );
		vels.Insert( at, vels[from] * 0.9f );
	}

	/// <summary>DISPLACE one onset by a cell, staying inside the allowed class — the same figure
	/// with one hit pushed or pulled. Not the first onset, for the same reason DROP spares it.
	/// </summary>
	/// <param name="offPulse">As <see cref="Add"/>'s: a kick may not be moved ONTO a beat either.
	/// The same hole a groove is defined by is just as fillable by a displaced push as by an added
	/// one — ska's beat 1 still sat 3 points over its baseline once Add alone was stopped. So the
	/// kick's on-beat set is frozen entirely: nothing enters it, nothing leaves it, and what
	/// arranges is the pushes around it.</param>
	void Displace( List<int> ticks, List<int> values, List<float> vels, Pattern fig, Skeleton sk,
		Rng rng, in ArrangeRole role, bool[] spine = null, bool offPulse = false )
	{
		if ( ticks.Count <= 1 ) return;
		int i = 1 + rng.Int( ticks.Count - 1 );
		int step = rng.Chance( 0.5f ) ? Skeleton.CellTicks : -Skeleton.CellTicks;
		// Both draws are taken before the spine is consulted, so a groove whose onsets are mostly
		// spine costs this stream exactly what one whose onsets are all free costs.
		if ( spine != null && spine[i] ) return;
		// The allowed class is often coarser than a sixteenth, so widen the move rather than giving
		// up: a skank displaced by one cell can never be legal, displaced by two it is.
		for ( int k = 1; k <= 4; k++ )
		{
			int t = ticks[i] + step * k;
			if ( t <= 0 || t >= fig.LengthTicks || ticks.Contains( t ) ) continue;
			if ( offPulse && DrumGroove.IsPulse( t ) ) continue;
			if ( !AllowedFigTick( t, fig, sk, role ) ) continue;
			// The VALUE and the VELOCITY move with the tick. A displace that moved only the tick
			// would keep the figure's cells and lose which hit was which — the chop that got pushed
			// would arrive wearing the next chop's articulation.
			int v = values[i]; float g = vels[i];
			ticks.RemoveAt( i ); values.RemoveAt( i ); vels.RemoveAt( i );
			int at = 0;
			while ( at < ticks.Count && ticks[at] < t ) at++;
			ticks.Insert( at, t ); values.Insert( at, v ); vels.Insert( at, g );
			return;
		}
	}

	/// <summary>RECOMBINE: take one bar of the phrase from another figure in the same genre's
	/// table. The genre's own vocabulary, re-cut — which is why this is the mutation that reaches
	/// furthest without ever producing a gesture the genre does not have.</summary>
	/// <param name="spine">Kept where the bar is cleared. Recombine is the one mutation that
	/// removes onsets it never looked at — it replaces a whole bar — so without this a genre's
	/// backbeat would survive Drop and Displace and then vanish anyway one time in six.</param>
	void Recombine( List<int> ticks, List<int> values, List<float> vels, Pattern fig, Rng rng,
		Pattern[] table, bool[] spine = null )
	{
		if ( table == null || table.Length < 2 ) return;
		Pattern other = null;
		for ( int tries = 0; tries < 4 && other == null; tries++ )
		{
			var p = table[rng.Int( table.Length )];
			if ( !ReferenceEquals( p, fig ) ) other = p;
		}
		if ( other == null ) return;

		int barTicks = _time.BarTicks;
		int bars = Math.Max( 1, fig.LengthTicks / barTicks );
		int bar = rng.Int( bars );
		int from = bar * barTicks, to = from + barTicks;

		for ( int i = ticks.Count - 1; i >= 0; i-- )
			if ( ticks[i] >= from && ticks[i] < to && (spine == null || !spine[i]) )
			{ ticks.RemoveAt( i ); values.RemoveAt( i ); vels.RemoveAt( i ); }

		// ONE bar of the other figure, folded onto this bar — and taken from ONE of its bars, not
		// every bar of it collapsed together. `tick % barTicks` maps a two-bar figure's second bar
		// back onto its first, so reading the whole thing would interleave two bars' onsets into
		// one and hand back a list that no longer ascends.
		int otherBar = (rng.Int( Math.Max( 1, other.LengthTicks / barTicks ) )) * barTicks;
		for ( int i = 0; i < other.Count; i++ )
		{
			int ot = other.TickAt( i );
			if ( ot < otherBar || ot >= otherBar + barTicks ) continue;
			int t = ot - otherBar + from;
			if ( t < from || t >= to || ticks.Contains( t ) ) continue;
			int at = 0;
			while ( at < ticks.Count && ticks[at] < t ) at++;
			ticks.Insert( at, t ); values.Insert( at, other.ValueAt( i ) ); vels.Insert( at, other.VelAt( i ) );
		}
	}
}