Pattern and Hit types for a music engine. Hit is an immutable onset record with tick, value, span and velocity. Pattern stores a looped rhythmic figure at tick resolution, provides builders (Eighths, Sixteenths, ThirtySeconds, Grid) and Slice() to map pattern onsets into song time given an anchor and feel.
using System;
using System.Collections.Generic;
namespace Skafinity;
/// <summary>One onset from a <see cref="Pattern"/>, mapped onto the song's own tick line.</summary>
readonly struct Hit
{
/// <summary>Absolute song tick of the onset.</summary>
public readonly int Tick;
/// <summary>The pattern's payload — what the cell means is the voice's business (a bass
/// interval, a drum accent flag, a comp figure's chord slot).</summary>
public readonly int Value;
/// <summary>Ticks until the pattern's next onset — the note's legato length, already scaled
/// out of pattern time into song time.</summary>
public readonly int SpanTicks;
/// <summary>Velocity multiplier authored into the cell (1 = the voice's nominal level).</summary>
public readonly float Vel;
public Hit( int tick, int value, int spanTicks, float vel )
{ Tick = tick; Value = value; SpanTicks = spanTicks; Vel = vel; }
}
/// <summary>
/// A rhythmic figure with its OWN length, free-running against the bar line.
///
/// This is the fix for sameyness inside a song. A table indexed by bar position can only ever
/// be one bar long, so bar 2 is bar 1 and every bar after it; a pattern that carries
/// <see cref="LengthTicks"/> is looped against absolute song time instead, and one mechanism
/// covers every case the engine wants:
///
/// • 1 bar — the old behaviour, unchanged.
/// • 2 bars — call and response: bar 2 answers bar 1 (the ska horn convention).
/// • 4 bars — a four-bar phrase for free, with the variation authored into its last bar.
/// • 3 eighths— a hemiola: the figure and the bar line drift apart and re-converge (see
/// <see cref="MusicGen"/>'s metric-dissonance handling).
///
/// Positions are ticks (see <see cref="Timing"/>), so a pattern is meter-agnostic: nothing here
/// assumes a bar, a beat count, or an eighth grid. <see cref="Eighths"/> is a convenience for
/// the tables that are still authored one cell per eighth.
///
/// Patterns are immutable and shared between songs — everything per-song (which pattern, where
/// it is anchored) is the caller's.
/// </summary>
sealed class Pattern
{
/// <summary>Cell value meaning "no onset here" in an eighth-authored table.</summary>
public const int Rest = Harmony.Rest;
/// <summary>The figure's own length. The bar line is not involved.</summary>
public readonly int LengthTicks;
readonly int[] _tick;
readonly int[] _value;
readonly int[] _span; // ticks to the next onset, wrapping around the loop
readonly float[] _vel;
public int Count => _tick.Length;
// Read access to the authored cells. Slice() is how a VOICE reads a pattern — it wants song
// time, an anchor and a feel. These are for the diagnostics, which want the figure itself:
// what rhythm was written, how long each note is, what degree it sings.
public int TickAt( int i ) => _tick[i];
public int ValueAt( int i ) => _value[i];
public int SpanAt( int i ) => _span[i];
public float VelAt( int i ) => _vel[i];
/// <summary>Onsets, in ticks from the pattern's own start. Must be sorted and inside
/// <paramref name="lengthTicks"/>.</summary>
public Pattern( int lengthTicks, int[] ticks, int[] values, float[] vels = null )
{
LengthTicks = Math.Max( 1, lengthTicks );
_tick = ticks; _value = values;
_vel = vels ?? Filled( ticks.Length, 1f );
_span = new int[ticks.Length];
for ( int i = 0; i < ticks.Length; i++ )
{
int next = i + 1 < ticks.Length ? _tick[i + 1] : _tick[0] + LengthTicks;
_span[i] = Math.Max( 1, next - _tick[i] );
}
}
static float[] Filled( int n, float v )
{
var a = new float[n];
for ( int i = 0; i < n; i++ ) a[i] = v;
return a;
}
/// <summary>Build from a table authored one cell per eighth — the shape most of the engine's
/// tables are still written in. <see cref="Rest"/> cells carry no onset (the previous note
/// sustains through them), which is what gives a cell its span.</summary>
public static Pattern Eighths( params int[] cells ) => Eighths( null, cells );
/// <summary>As <see cref="Eighths(int[])"/>, with a velocity per cell (null = all 1).</summary>
public static Pattern Eighths( float[] vels, params int[] cells )
{
var t = new List<int>(); var v = new List<int>(); var g = new List<float>();
for ( int e = 0; e < cells.Length; e++ )
{
if ( cells[e] == Rest ) continue;
t.Add( e * Timing.TicksPerEighth );
v.Add( cells[e] );
g.Add( vels != null && e < vels.Length ? vels[e] : 1f );
}
return new Pattern( cells.Length * Timing.TicksPerEighth, t.ToArray(), v.ToArray(), g.ToArray() );
}
/// <summary>Build from sixteenth cells — the resolution the metal gallop and the country train
/// beat are actually authored at.</summary>
public static Pattern Sixteenths( params int[] cells ) => Grid( Timing.TicksPerEighth / 2, cells );
/// <summary>Build from THIRTY-SECOND cells — a tremolo-picked riff or a double-kick run.
///
/// <see cref="Timing.TicksPerBeat"/> is 48, so a 32nd is exactly 6 ticks: the same clean
/// division that gives a sixteenth 12 and a sixteenth-triplet 8, and it sits on the grid the
/// suite measures against. Nothing gates where a figure this fine may be played — not a genre
/// and not a tempo. What keeps it playable is the GESTURE: 32nds are authored as bursts that
/// resolve into the next beat, and a burst is as playable at the top of a genre's band as at
/// the bottom. A bar of unbroken 32nds is not, and that is an authoring judgement.
/// </summary>
public static Pattern ThirtySeconds( params int[] cells ) => Grid( Timing.TicksPerEighth / 4, cells );
static Pattern Grid( int step, int[] cells )
{
var t = new List<int>(); var v = new List<int>();
for ( int s = 0; s < cells.Length; s++ )
{
if ( cells[s] == Rest ) continue;
t.Add( s * step ); v.Add( cells[s] );
}
return new Pattern( cells.Length * step, t.ToArray(), v.ToArray() );
}
/// <summary>
/// The onsets that land in <c>[fromTick, toTick)</c> of SONG time.
///
/// The pattern loops from <paramref name="anchorTick"/> (a section's first downbeat, so a
/// multi-bar figure restarts with the section rather than wherever the song happens to be).
/// <paramref name="feel"/> is the section's half/double-time multiplier — the pattern's own
/// rate, not the tempo: 0.5 stretches every figure to twice its length, 2 halves it.
///
/// There is deliberately NO "shift every onset late" argument. A figure that pushes into the
/// next bar says so in its CELLS — the ska skank's stab on the "and of 4", the Charleston, the
/// horn answer — which is one gesture at a phrase seam, and that is where a push belongs. A
/// section-wide constant offset expressed the same idea the other way and it did not survive a
/// listen; see the note in <see cref="SongForm"/>.
/// </summary>
public List<Hit> Slice( int fromTick, int toTick, int anchorTick = 0, float feel = 1f )
{
var hits = new List<Hit>();
if ( toTick <= fromTick || _tick.Length == 0 ) return hits;
if ( feel <= 0f ) feel = 1f;
// Song tick t ↔ pattern tick p: p = (t − anchor) · feel. The window is converted once,
// then walked repetition by repetition; every hit maps back the same way.
double pFrom = (fromTick - anchorTick) * (double)feel;
double pTo = (toTick - anchorTick) * (double)feel;
long rep = (long)Math.Floor( pFrom / LengthTicks );
for ( ; ; rep++ )
{
double bas = (double)rep * LengthTicks;
if ( bas >= pTo ) break;
for ( int i = 0; i < _tick.Length; i++ )
{
double p = bas + _tick[i];
if ( p < pFrom || p >= pTo ) continue;
int t = anchorTick + (int)Math.Round( p / feel );
int span = Math.Max( 1, (int)Math.Round( _span[i] / feel ) );
hits.Add( new Hit( t, _value[i], span, _vel[i] ) );
}
}
return hits;
}
}