Engine/Voices/Lead.cs

Part of the MusicGen engine, this file implements the lead instrument phrase generation and rendering. It contains routines for different lead styles (sung melody, shred, double-stop, unison), voice selection (trumpet/sax/organ/trombone), note run and ornament logic, and dispatches notes to patch/voicing and rendering functions.

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

using static Skafinity.Osc;

namespace Skafinity;

// The lead line. One phrase generator served every genre — same rest chance, same leap logic,
// same register, same "a phrase every two bars" — which made the melody the most interchangeable
// thing in the song. There is a grammar per genre now (see LeadStyle); the VOICES below are
// unchanged, because a trumpet is a trumpet whatever it plays.
//
// Part of the MusicGen engine — see MusicGen.cs.

// ── Lead instrument voices ──
enum Instrument { Trumpet, Sax, Organ, Trombone }

public sealed partial class MusicGen
{
	/// <summary>One lead phrase, in the genre's own grammar.</summary>
	void RenderLeadPhrase( int barTick, int barTicks, int chord, Rng rng, Rng exprRng )
	{
		if ( rng.Chance( _prof.LeadSilence ) ) return;              // the phrase rests
		int span = Math.Max( 1, _prof.LeadPhraseBars ) * barTicks;
		switch ( _prof.Lead )
		{
			case LeadStyle.Shred: RenderShredPhrase( barTick, span, chord, rng, exprRng ); break;
			case LeadStyle.DoubleStop: RenderDoubleStopPhrase( barTick, span, chord, rng, exprRng ); break;
			case LeadStyle.Unison: RenderUnisonPhrase( chord, exprRng ); break;
			// Pop's Hook style is the tune everywhere (see Melody) — where a pop section has no
			// tune to sing it lays out rather than improvising, which is what a drop is.
			case LeadStyle.Hook:
			case LeadStyle.Bluesy: RenderSungPhrase( barTick, span, chord, rng, exprRng, sparse: true ); break;
			default: RenderSungPhrase( barTick, span, chord, rng, exprRng, sparse: false ); break;
		}
	}

	/// <summary>The lead's register, per grammar.
	///
	/// THE MELODY NEEDS AN OCTAVE OF ITS OWN. The comp registers are fixed and known — the rhythm
	/// guitar voices its chord over <c>_rootMidi + 12</c>, the skank and the keys over
	/// <c>+24</c> — so a lead based at +21 or +24 sang its whole line inside the chordal bed and
	/// simply disappeared into it, which no amount of gain fixes. +31 clears the top of that bed
	/// (a full voicing reaches about +31) and the tune's own degrees carry it up from there.
	///
	/// The two exceptions are relational rather than absolute: metal's shred already sat clear at
	/// +26 and ranges far wider than a tune does, and punk's unison IS the riff an octave up, so
	/// its base has to stay one octave over the guitar's <c>_rootMidi + 12</c> or it stops being
	/// a double.</summary>
	// Three octaves for the sung line: the comp registers are fixed and known — the rhythm guitar
	// voices its chord one octave up, the skank and the keys two, a full voicing reaching about
	// two and a half — so a melody at two octaves sings inside that bed and no amount of gain
	// brings it out. Metal's shred ranges far wider than a tune and its own comp sits at the root,
	// so two clears it; punk's unison IS the riff an octave over the guitar's one, so it is two by
	// definition. See Register: these are octaves because a register cannot be anything else.
	int LeadBase() => Register( _prof.Lead switch
	{
		LeadStyle.Shred => 2,
		LeadStyle.Unison => 2,
		_ => 3,
	} );

	// ── Ska-punk (and rock, sparser): a sung line ──
	// Chord-tone locked on the strong beats so it stays consonant, stepping or leaping between
	// them. This is the old generator, kept for the two genres whose lead really is a melody —
	// with ska phrasing its second half as an ANSWER to its first (the horn convention), and rock
	// leaving far more space than it plays.
	void RenderSungPhrase( int barTick, int span, int chord, Rng rng, Rng exprRng, bool sparse )
	{
		int melBase = LeadBase();
		var tones = ChordDegrees( chord );
		int degree = tones[rng.Int( Math.Min( 3, tones.Length ) )];
		bool guitarLead = !_hornLead;
		float amp = guitarLead ? _c.LeadGtrVol * _c.LeadGtrBalance : _c.MelodyVol * _c.MelodyBalance;
		amp *= _midMul;
		float drive = guitarLead ? _c.LeadGtrDrive : _c.MelodyDrive;
		float rest = _c.MelodyRestChance * (sparse ? 1.6f : 1f);
		float tripChance = sparse ? 0f : _c.TripletChance;
		var ex = guitarLead ? Expr( "LEAD GTR" ) : Expr( "LEAD" );
		int prevMidi = NoPrev;

		int t = barTick;
		int half = barTick + span / 2;
		while ( t < barTick + span )
		{
			if ( rng.Chance( rest ) ) { t += Timing.TicksPerEighth; continue; }

			if ( rng.Chance( tripChance ) )
			{
				t = RenderLeadRun( t, chord, degree, melBase, amp, drive, ex, rng, exprRng, ref prevMidi );
				continue;
			}

			int len = (1 + rng.Int( 3 )) * Timing.TicksPerEighth;
			if ( t + len > barTick + span ) len = barTick + span - t;
			bool strong = ((t - barTick) / Timing.TicksPerEighth) % 2 == 0;

			// The answer half of a horn line lands a step lower than the call did — same shape,
			// resolved — rather than re-rolling as if it were a new phrase. That step is meant to
			// leave the chord tone, so it is the one strong beat the sounding snap must not undo.
			bool onChordTone = false;
			if ( strong )
			{
				int answer = t >= half && !sparse ? 1 : 0;
				degree = NearestChordTone( tones, degree ) - answer;
				onChordTone = answer == 0;
			}
			else
			{
				int step = rng.Chance( _c.MelodyLeapChance ) ? (rng.Chance( 0.5f ) ? 3 : -3)
					: (rng.Chance( 0.5f ) ? 1 : -1);
				degree = Math.Clamp( degree + step, _prog[chord] - 3, _prog[chord] + 10 );
			}

			int midi = ScaleMidi( melBase, degree );
			if ( onChordTone ) midi = NearestSoundingTone( midi, chord, t );
			var vc = Roll( ex, midi, prevMidi, exprRng, (float)_time.SpanSeconds( t, len ),
				BendBias( len, (t - barTick) / (float)span ) );
			RenderLeadNote( _time.TickToSample( t ), _time.SpanSamples( t, len * 0.9 ), midi,
				amp * NoteGain( 1f ), _time.SpanSeconds( t, len ) * 0.7, drive, vc );
			prevMidi = midi;
			t += Math.Max( Timing.TicksPerEighth, len );
		}
	}

	/// <summary>Metal: scalar shred. Runs straight up and down the mode across the whole register —
	/// not the sung line's chord-tone hops with a triplet ornament on top.
	///
	/// The run is sixteenths, with BURSTS of thirty-seconds inside it — which is what a run
	/// actually is on a fretboard. A player does not pick every note of a fast line: they pick one
	/// and hammer-on or pull-off the next, or sweep across strings, so a burst costs the hand far
	/// less than its note count suggests and is over before it runs out. That is why the burst is
	/// short and the surrounding line is not, and it is why nothing here gates on the tempo: the
	/// gesture is a handful of notes wherever it lands.</summary>
	void RenderShredPhrase( int barTick, int span, int chord, Rng rng, Rng exprRng )
	{
		int melBase = LeadBase();
		float amp = _c.LeadGtrVol * _c.LeadGtrBalance * _midMul;
		var ex = Expr( "LEAD GTR" );
		int prevMidi = NoPrev;
		int sixteenth = Timing.TicksPerEighth / 2, thirtySecond = Timing.TicksPerEighth / 4;
		int degree = _prog[chord];
		int dir = rng.Chance( 0.5f ) ? 1 : -1;
		int burst = 0;                                    // notes left in the current 32nd flurry

		for ( int t = barTick; t < barTick + span; )
		{
			// This note's value: inside a flurry it is a 32nd, otherwise a 16th — and a flurry
			// always STARTS on a 16th, four to eight notes long, so it reads as an acceleration
			// out of the line rather than as a second tempo.
			int step = burst > 0 ? thirtySecond : sixteenth;
			if ( burst > 0 ) burst--;
			else if ( rng.Chance( 0.18f ) ) burst = 4 + 2 * rng.Int( 3 );

			// A run breathes at the phrase's seams, and turns around when it runs out of register.
			if ( rng.Chance( 0.12f ) ) { dir = -dir; t += step; continue; }
			degree += dir;
			if ( degree > _prog[chord] + 14 || degree < _prog[chord] - 7 ) { dir = -dir; degree += 2 * dir; }
			int midi = ScaleMidi( melBase, degree );
			var vc = Roll( ex, midi, prevMidi, exprRng, (float)_time.SpanSeconds( t, step ),
				BendBias( step, (t - barTick) / (float)span ) );
			RenderLeadNote( _time.TickToSample( t ), _time.SpanSamples( t, step * 0.95 ), midi,
				amp * NoteGain( 1f ), _time.SpanSeconds( t, step ) * 0.8,
				_c.LeadGtrDrive, vc );
			prevMidi = midi;
			t += step;
		}
	}

	/// <summary>Country: a single-note line with double-stops as punctuation — the Telecaster move,
	/// and the reason country's lead reads as country even over the same changes.
	///
	/// PUNCTUATION, not the voice. Harmonising every note in parallel thirds is not a guitar part:
	/// two pitches, held the same length, bent and vibratoed together, is a two-tone horn, and it
	/// leaves no single line for the ear to follow. <see cref="DoubleStopChance"/> is what makes it
	/// a lick.</summary>
	void RenderDoubleStopPhrase( int barTick, int span, int chord, Rng rng, Rng exprRng )
	{
		int melBase = LeadBase();
		float amp = _c.LeadGtrVol * _c.LeadGtrBalance * _midMul;
		var ex = Expr( "LEAD GTR" );
		var tones = ChordDegrees( chord );
		int prevMidi = NoPrev;
		int degree = tones[rng.Int( tones.Length )];

		for ( int t = barTick; t < barTick + span; )
		{
			int len = (1 + rng.Int( 3 )) * Timing.TicksPerEighth;
			if ( t + len > barTick + span ) len = barTick + span - t;
			if ( rng.Chance( 0.25f ) ) { t += len; continue; }       // country leaves space

			int midi = ScaleMidi( melBase, degree );
			var vc = Roll( ex, midi, prevMidi, exprRng, (float)_time.SpanSeconds( t, len ),
				BendBias( len, (t - barTick) / (float)span ) );
			float gain = amp * NoteGain( 1f );
			RenderLeadNote( _time.TickToSample( t ), _time.SpanSamples( t, len * 0.9 ), midi, gain,
				_time.SpanSeconds( t, len ) * 0.75, _c.LeadGtrDrive, vc );
			if ( rng.Chance( DoubleStopChance ) ) EmitDoubleStop( t, len, degree, gain );
			prevMidi = midi;

			degree = rng.Chance( 0.5f ) ? NearestChordTone( tones, degree + (rng.Chance( 0.5f ) ? 2 : -2) )
				: degree + (rng.Chance( 0.5f ) ? 1 : -1);
			// Held inside the phrase's own register. The walk had no bound at all, so a long solo
			// wandered off the top or the bottom of the line it started on and never came back —
			// which is the same "where is the melody" the parallel thirds caused.
			degree = Math.Clamp( degree, _prog[chord] - 3, _prog[chord] + 10 );
			t += Math.Max( Timing.TicksPerEighth, len );
		}
	}

	/// <summary>How often a country lead note is picked as a double-stop rather than played alone.
	/// It is an ornament: on every note it stops being one, and the line stops being a line.</summary>
	const float DoubleStopChance = 0.35f;

	/// <summary>The harmony note of a country double-stop: the diatonic third UNDER the melody
	/// note, picked with it and left to fall away.
	///
	/// UNDER, because the melody has to stay the top line — harmonising above makes the ornament
	/// the highest voice, and the ear follows the top, so the line a listener hears is the harmony
	/// rather than the tune. DRY, because the melody note's bend/scoop/vibrato is a fretting hand
	/// working ONE string: sliding both notes of a dyad in parallel is the horn. And SHORTER,
	/// because it is picked with the melody note, not held alongside it.</summary>
	void EmitDoubleStop( int tick, int lenTicks, int degree, float gain )
		=> RenderLeadNote( _time.TickToSample( tick ), _time.SpanSamples( tick, lenTicks * 0.6 ),
			ScaleMidi( LeadBase(), degree - 2 ), gain * 0.55f,
			_time.SpanSeconds( tick, lenTicks ) * 0.45, _c.LeadGtrDrive, default );

	/// <summary>Punk: unison. When the lead plays at all it doubles the riff an octave up, which
	/// is what a second guitarist in a three-piece actually does. Falls silent when there is no
	/// riff to double (its LeadSilence is high, so that is most phrases anyway).</summary>
	void RenderUnisonPhrase( int chord, Rng exprRng )
	{
		if ( _riffOnsets.Count == 0 ) return;
		int melBase = LeadBase();
		float amp = _c.LeadGtrVol * _c.LeadGtrBalance * _midMul * 0.8f;
		var ex = Expr( "LEAD GTR" );
		int prev = NoPrev;
		foreach ( var h in _riffOnsets )
		{
			if ( h.Value == CompFigure.Mute ) continue;              // the muted chug is not a note
			int midi = ScaleMidi( melBase, _prog[chord] );
			// The unison has no phrase of its own — it is the riff's — so the bar is the phrase.
			var vc = Roll( ex, midi, prev, exprRng, (float)_time.SpanSeconds( h.Tick, h.SpanTicks ),
				BendBias( h.SpanTicks, (h.Tick - _barTick) / (float)_time.BarTicks ) );
			prev = midi;
			RenderLeadNote( _time.TickToSample( h.Tick ), _time.SpanSamples( h.Tick, h.SpanTicks * 0.9 ),
				midi, amp * NoteGain( h.Vel ), _time.SpanSeconds( h.Tick, h.SpanTicks ) * 0.7,
				_c.LeadGtrDrive, vc );
		}
	}

	/// <summary>A run of evenly-spaced notes around the current degree — the sung line's ornament.
	/// Returns the tick the phrase continues from.</summary>
	int RenderLeadRun( int t, int chord, int degree, int melBase, float amp, float drive,
		in Expression ex, Rng rng, Rng exprRng, ref int prevMidi )
	{
		float r = rng.Next();
		int n, spanTicks;
		if ( r < 0.25f ) { n = 2; spanTicks = Timing.TicksPerEighth; }
		else if ( r < 0.50f ) { n = 3; spanTicks = Timing.TicksPerEighth; }
		else if ( r < 0.80f ) { n = 3; spanTicks = Timing.TicksPerBeat; }
		else { n = 3; spanTicks = Timing.TicksPerBeat * 2; }

		int first = ScaleMidi( melBase, Math.Clamp( degree - n / 2, _prog[chord] - 3, _prog[chord] + 10 ) );
		var runVc = Roll( ex, first, prevMidi, exprRng );
		for ( int k = 0; k < n; k++ )
		{
			int d2 = Math.Clamp( degree + (k - n / 2), _prog[chord] - 3, _prog[chord] + 10 );
			int m2 = ScaleMidi( melBase, d2 );
			// A tuplet divides its own span evenly — it is not warped onto the shuffle grid a
			// second time (see Timing.EvenSpan).
			RenderLeadNote( _time.EvenSpan( t, spanTicks, k / (double)n ),
				_time.SpanSamples( t, spanTicks / (double)n * 0.9 ), m2, amp * NoteGain( 1f ),
				_time.SpanSeconds( t, spanTicks ) / n * 0.85, drive, runVc );
			prevMidi = m2;
		}
		return t + spanTicks;
	}

	/// <summary>The chord tone nearest <paramref name="degree"/>, in degree space.</summary>
	/// <summary>Snap a melody PITCH to the nearest pitch the chord actually sounds.
	///
	/// The melodic view of a chord is diatonic (<see cref="ChordDegrees"/> — the root degree plus
	/// the voicing's offsets) and the SOUNDING one is not: <see cref="Harmony.VoicedTone"/> forces
	/// the fourth and the fifth perfect, because a perfect fourth is what a sus4 IS. Every scale
	/// has one degree whose diatonic fourth is augmented, and on that degree the two views sit a
	/// semitone apart — so a note the composer deliberately resolved to a chord tone arrives a
	/// semitone off the chord, which is the one place a "consonant on the strong beats" melody can
	/// still grind. Snapping in degree space gets the note close; this puts it on the chord.
	///
	/// Pitch is the only view that cannot disagree with what is playing, and because
	/// <see cref="ChordMidis"/> is tick-aware the melody follows a suspension to its resolution
	/// too. Octave is preserved — this moves a note by a semitone or two, never registers.</summary>
	int NearestSoundingTone( int midi, int chord, int tick )
	{
		static int Pc( int x ) => ((x % 12) + 12) % 12;
		int best = midi, bestD = int.MaxValue;
		foreach ( var t in ChordMidis( Register( 0 ), chord, tick ) )
		{
			int up = midi + Pc( Pc( t ) - Pc( midi ) );
			foreach ( var cand in new[] { up, up - 12 } )
			{
				int d = Math.Abs( cand - midi );
				if ( d < bestD ) { bestD = d; best = cand; }
			}
		}
		return best;
	}

	int NearestChordTone( int[] tones, int degree )
	{
		int best = tones[0], bestD = int.MaxValue;
		foreach ( var t in tones )
			for ( int oc = -_scale.Length; oc <= 2 * _scale.Length; oc += _scale.Length )
			{
				int cand = t + oc;
				int dist = Math.Abs( cand - degree );
				if ( dist < bestD ) { bestD = dist; best = cand; }
			}
		return best;
	}

	/// <summary>Per-genre lead level, measured with `--levels` and re-measured after any change to
	/// what the lead plays.
	///
	/// The target is +2 dB over that genre's drums, not level with them. The mix rebalance set
	/// these against the kit while the lead was still filling the odd phrase; it carries the tune
	/// now, and a melody that sits AT kit level is a melody a listener has to go looking for.
	/// Every genre measured within a dB of 0 before this, so each is a straight trim to +2 —
	/// which is also comfortably inside the suite's "the lead does not dominate" ceiling.
	///
	/// Re-measured when the metric accent stopped reaching pitched voices (see NoteGain), which is
	/// a level change and an UNEVEN one: country's and ska's accent weights all sit above 1, so
	/// their voices lost level, while rock's and pop's sit below and theirs gained. Punk's is the
	/// one trim that is not about that change — it had been sitting at the kit's own level rather
	/// than +2 since well before it, and a fresh measurement is what turned that up.</summary>
	float LeadLevel() => _genre switch
	{
		0 => 1.19f,   // ska horn — the third-wave kit is brighter and louder than the one before it
		1 => 0.80f,   // rock
		2 => 0.67f,   // country: one note, with the odd double-stop under it
		3 => 1.21f,   // metal: it is supposed to be on top
		4 => 0.81f,   // punk: it doubles the guitar, and two of everything is loud
		_ => 1.25f,   // pop: the hook IS the song
	};

	// Dispatch a lead note to the genre's lead voice: a distorted single-note guitar for rock,
	// otherwise the ska horn (RenderLead → trumpet).
	void RenderLeadNote( int at, int dur, int midi, float amp, double decaySec, float drive, in Voicing vc )
	{
		amp *= LeadLevel();
		if ( !_hornLead )
		{
			// Twang = a bright cutoff-envelope snap on each pick (high CutEnv, decays fast) through
			// a resonant SVF, plus a BASE distortion under the slider so it reads as an electric
			// guitar even at the slider minimum. The base is genre-set: rock = 3 (overdriven),
			// metal = 4 hot (heavy), country = clean (the bite comes from the twang snap + bends,
			// not gain). The bends (BENDINESS knob → bend-in + scoop) come in via the voicing.
			float driveAmt = _genre switch
			{
				3 => 4f + MathF.Max( 1f, _c.LeadGtrDrive ),         // metal: heavy
				2 => 0.8f + 0.3f * MathF.Max( 1f, _c.LeadGtrDrive ),// country: clean twang
				4 => 2f + MathF.Max( 1f, _c.LeadGtrDrive ),         // punk: bright, lightly driven
				5 => 0.6f + 0.2f * MathF.Max( 1f, _c.LeadGtrDrive ),// pop: clean synth pluck/lead
				_ => 3f + MathF.Max( 1f, _c.LeadGtrDrive ),         // rock
			};
			// Country/pop get a brighter cutoff snap — country for telecaster twang, pop for a
			// plucky synth attack.
			float cutEnv = _genre == 2 ? 3000f : _genre == 5 ? 3500f : 2200f;
			var gtr = new Patch
			{
				Osc = 1, Voices = 1, Detune = 0f, Amp = amp,
				Attack = 0.002f, Decay = decaySec, Sustain = 0.55f, Sustained = true,
				Cutoff = _c.LeadGtrCutoff, CutEnv = cutEnv, Reso = 0.65f,
				Drive = driveAmt, Pan = _leadPan, Vibrato = _c.MelodyVibrato,
			};
			ApplyVoicing( ref gtr, vc );
			// The lead is monophonic — a single clean take at its per-song pan (_leadPan). It is NOT
			// double-tracked: splitting a solo line into two detuned, hard-panned, time-offset takes
			// beats on sustained notes and overlaps adjacent pitches on runs, reading as "out of key".
			// Double-tracking width is for the chordal/strummed voices, not the melody.
			RenderPatch( at, dur, Midi( midi ), gtr, mono: true );
			return;
		}
		RenderLead( at, dur, midi, amp, decaySec, drive, vc );
	}

	// Which voice takes the ska lead — a weighted draw, or the config's override. The draw is
	// taken either way: a knob that decides WHAT plays must not also decide how many values the
	// composer pulls, or overriding the instrument would quietly rewrite the rest of the song.
	Instrument PickInstrument( Rng rng )
	{
		float tw = MathF.Max( 0f, _c.TrumpetWeight ), sw = MathF.Max( 0f, _c.SaxWeight );
		float ow = MathF.Max( 0f, _c.OrganWeight ), bw = MathF.Max( 0f, _c.TromboneWeight );
		float sum = tw + sw + ow + bw;
		float r = rng.Next() * sum;

		if ( _c.ForceInstrument >= 0 && _c.ForceInstrument <= 3 ) return (Instrument)_c.ForceInstrument;
		if ( sum <= 0f ) return Instrument.Trumpet;
		if ( (r -= tw) < 0f ) return Instrument.Trumpet;
		if ( (r -= sw) < 0f ) return Instrument.Sax;
		if ( (r -= ow) < 0f ) return Instrument.Organ;
		return Instrument.Trombone;
	}

	void RenderLead( int at, int dur, int midi, float amp, double decaySec, float drive, in Voicing vc )
	{
		Patch p; int m = midi;
		switch ( _lead )
		{
			case Instrument.Trumpet:
				p = new Patch
				{
					Osc = 1, Voices = 3, Detune = _c.Detune * 0.7f, Amp = amp,
					Attack = 0.01f, Decay = decaySec, Sustain = 0.7f, Sustained = true,
					Cutoff = _c.LeadCutoff, CutEnv = 1800f, Reso = 1.0f, Drive = drive,
					Pan = _leadPan, Vibrato = _c.MelodyVibrato,
				};
				break;
			case Instrument.Trombone:
				m = midi - 12;
				p = new Patch
				{
					Osc = 1, Voices = 3, Detune = _c.Detune * 0.7f, Amp = amp * 1.1f,
					Attack = 0.02f, Decay = decaySec, Sustain = 0.7f, Sustained = true,
					Cutoff = _c.LeadCutoff * 0.7f, CutEnv = 900f, Reso = 1.0f, Drive = MathF.Max( 1f, drive * 0.8f ),
					Pan = _leadPan, Vibrato = _c.MelodyVibrato * 0.7f,
				};
				break;
			case Instrument.Sax:
				p = new Patch
				{
					Osc = 3, Voices = 2, Detune = _c.Detune * 0.5f, Amp = amp * 1.15f,
					Attack = 0.014f, Decay = decaySec, Sustain = 0.75f, Sustained = true,
					Cutoff = _c.LeadCutoff, CutEnv = 1400f, Reso = 0.7f, Drive = MathF.Max( 1.2f, drive ),
					Pan = _leadPan, Vibrato = _c.MelodyVibrato, Breath = 0.03f,
				};
				break;
			default: // Organ
				p = new Patch
				{
					Osc = 0, Voices = 3, Detune = _c.Detune * 0.6f, Amp = amp,
					Attack = 0.006f, Decay = decaySec * 1.5, Sustain = 0.9f, Sustained = true,
					Cutoff = 2600f, CutEnv = 0f, Reso = 1.0f, Drive = 1.15f,
					Pan = _leadPan, Vibrato = _c.MelodyVibrato * 0.9f,
				};
				break;
		}
		ApplyVoicing( ref p, vc );
		// Single clean take at _leadPan — the ska horn/organ lead is monophonic, so it is not
		// double-tracked (see the guitar-lead note in RenderLeadNote: doubling a solo line smears
		// pitch). Only chordal/strummed voices get the width.
		RenderPatch( at, dur, Midi( m ), p, mono: true );
	}
}