PlanTrace records when musical onsets occur per voice and the song's section spans. It stores per-voice lists of tick offsets, counts of struck hits, and a list of SectionSpan entries describing section timing, groove and kit patterns.
using System;
using System.Collections.Generic;
namespace Skafinity;
/// <summary>Which part an onset belongs to. Only the voices whose PLACEMENT is a composed
/// decision are here — the busy layer's ghosts and the kick-sync stray are per-bar rolls on top
/// of a groove rather than a part anyone wrote, and counting them would put a knob's setting into
/// a measurement of what the genre plays.</summary>
enum TraceVoice { Kick, Snare, Cymbal, Bass, Comp, Keys, Tune }
/// <summary>
/// Every onset a song composed, per voice, in TICKS.
///
/// This exists so a sweep over hundreds of songs can ask what the parts actually played without
/// re-deriving it. The alternative — walking the structure a second time and slicing the same
/// patterns — is a SECOND IMPLEMENTATION of every precedence rule in the composer (which figure a
/// bar plays: hemiola, then the loud figure, then the flourish, then the section's own; whether
/// the bass reads the riff; whether a section sings a tune at all), and two implementations of
/// that drift. When they drift the tool starts lying with a straight face, which is worse than
/// not having it — so the onsets are recorded by the voices themselves, at the moment they play,
/// and there is exactly one answer to what a bar played.
///
/// Attach one with <see cref="MusicGen.Trace"/> before composing. Null (the default) costs a
/// null check per bar per voice and nothing else.
/// </summary>
sealed class PlanTrace
{
const int Voices = 7;
/// <summary>One section, as the composer laid it out. Recorded rather than re-derived for the
/// same reason the onsets are: a sweep that walked the form itself would be a second bar ruler,
/// and it would be wrong exactly when the form varies per song — which it now does.</summary>
public readonly struct SectionSpan
{
public readonly int Tick, Ticks, BarTicks;
public readonly Section Type;
/// <summary>The groove the section drew, and the kit patterns it will actually play — which
/// are the drawn ones worked on, so the two are no longer the same thing. This is the kit's
/// PLAN; what the trace's onsets carry is the realisation, and the ghost roll and the fill
/// sit between them.</summary>
public readonly string Groove;
public readonly Pattern Kick, Snare, Cymbal;
public SectionSpan( int tick, int ticks, int barTicks, Section type,
string groove, Pattern kick, Pattern snare, Pattern cymbal )
{
Tick = tick; Ticks = ticks; BarTicks = barTicks; Type = type;
Groove = groove; Kick = kick; Snare = snare; Cymbal = cymbal;
}
}
/// <summary>The song's sections in order. What makes a per-SECTION question askable at all —
/// the onsets below are one flat list per voice and carry no boundaries of their own.</summary>
public readonly List<SectionSpan> Sections = new();
public void Mark( int tick, int ticks, int barTicks, Section type,
string groove, Pattern kick, Pattern snare, Pattern cymbal )
=> Sections.Add( new SectionSpan( tick, ticks, barTicks, type, groove, kick, snare, cymbal ) );
readonly List<int>[] _ticks = new List<int>[Voices];
public PlanTrace()
{
for ( int i = 0; i < Voices; i++ ) _ticks[i] = new List<int>();
}
/// <summary>The onsets one voice played, in composition order (which is bar order).</summary>
public List<int> Of( TraceVoice v ) => _ticks[(int)v];
public void Add( TraceVoice v, int tick ) => _ticks[(int)v].Add( tick );
/// <summary>An onset that is a STRUCK hit rather than a ghost. Ghosts are half of what a snare
/// plays in three of these genres, so a count that does not separate them says "punk plays a
/// lot of snare" where the tell is "punk STRIKES two of them and ghosts the rest".</summary>
public void Add( TraceVoice v, int tick, bool struck )
{
_ticks[(int)v].Add( tick );
if ( struck ) _struck[(int)v]++;
}
readonly int[] _struck = new int[Voices];
public int Struck( TraceVoice v ) => _struck[(int)v];
/// <summary>Record a bar's worth of sliced hits.</summary>
public void Add( TraceVoice v, List<Hit> hits )
{
var list = _ticks[(int)v];
for ( int i = 0; i < hits.Count; i++ ) list.Add( hits[i].Tick );
}
}