Code/Engine/PlanTrace.cs

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.

File AccessNetworking
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 );
	}
}