Code/Engine/Voices/Horns.cs

Part of the MusicGen engine, this file implements the horn backing section renderer. It chooses chord tones, panning spread, dynamics and ornamentation then schedules horn patch notes and stabs with variations like arpeggios, 16th pairs and grace pickups.

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

using static Skafinity.Osc;

namespace Skafinity;

// The backing horn section, panned across the stereo field.
//
// Part of the MusicGen engine — see MusicGen.cs.

public sealed partial class MusicGen
{
	// ── Backing horns (panned spread) ──
	// The figure is a TWO-BAR call and response (see MusicGen.HornFigure): the section states a
	// line and then answers it, which is what a horn section does and what a single reused
	// eight-slot mask could never express. On top of that a dedicated stream breaks the stabs up
	// with rolling arpeggios, 16th pairs and grace pickups so even a repeated call is played
	// slightly differently.
	void RenderHornStabs( int barTick, int barTicks, int chord, Rng orn, Rng exprRng )
	{
		int spe = _time.Spe;
		int baseMidi = Register( 2 );
		var tones = ChordMidis( baseMidi, chord, barTick );
		float spread = _c.PanAmount * 0.7f;
		float sectionGain = TuneFor( _sectionType ) != null ? 0.7f : 1f;
		int six = spe / 2;
		float ornChance = 0.18f + _c.TripletChance; // ~0.24 default; rides the same knob

		// Section expression — vibrato + a chance of a scoop/fall, rolled once per onset and
		// applied to every tone of that stab so the whole section bends together.
		var ex = Expr( "HORNS" );
		Voicing hornVc = default;
		float gainNow = 1f;
		int tickNow = barTick;

		// one chord-tone voice
		void Note( int at, int dur, int k, double dec, float gain )
		{
			var horn = new Patch
			{
				Osc = 1, Voices = 3, Detune = _c.Detune,
				Amp = _c.HornVol * _c.HornBalance * _midMul / tones.Length * gain * sectionGain
					* NoteGain( gainNow ),
				Attack = 0.008f, Decay = dec,
				Sustain = 0.2f, Sustained = false,
				Cutoff = _c.HornCutoff, CutEnv = 1200f, Reso = 1.0f,
				Drive = _c.HornDrive,
				Pan = spread * (k / (float)Math.Max( 1, tones.Length - 1 ) * 2f - 1f),
				Vibrato = _c.MelodyVibrato,
			};
			ApplyVoicing( ref horn, hornVc );
			RenderPatch( at, dur, Midi( tones[k] ), horn );
		}

		// full block chord stab
		void Stab( int at, int dur, double dec, float gain )
		{
			for ( int k = 0; k < tones.Length; k++ ) Note( at, dur, k, dec, gain );
		}

		// When the section is singing a tune, the horns ANSWER it — they do not double it. The
		// lead and the section are the same instrument in nearly the same register, so a full
		// two-bar stab figure under a horn melody reads as two lead lines fighting. The call bar
		// belongs to the melody; the horns take the answer bar, quieter.
		bool answerOnly = TuneFor( _sectionType ) != null;

		foreach ( var h in _hornFig.Slice( barTick, barTick + barTicks, _sectionTick, _feel ) )
		{
			if ( answerOnly && ((h.Tick - _sectionTick) / barTicks) % 2 == 0 ) continue;
			int at = _time.TickToSample( h.Tick );
			double secPerEighth = _time.SpanSeconds( h.Tick, Timing.TicksPerEighth );
			tickNow = h.Tick; gainNow = h.Vel;
			hornVc = Roll( ex, baseMidi, NoPrev, exprRng );

			if ( six > 0 && orn.Chance( ornChance ) )
			{
				float r = orn.Next();
				if ( r < 0.4f )
				{
					// rolling arpeggio: chord tones climb across a 16th-triplet
					int step = spe / 3;
					for ( int k = 0; k < tones.Length; k++ )
						Note( _time.EvenSpan( h.Tick, Timing.TicksPerEighth, k / 3.0 ),
							(int)(step * 0.9f), k, secPerEighth / 3 * 0.8, 1f );
					continue;
				}
				if ( r < 0.75f )
				{
					// 16th pair: stab on the beat, softer echo on the "e"
					Stab( at, (int)(six * 0.85f), secPerEighth * 0.5 * 0.8, 1f );
					Stab( _time.TickToSample( h.Tick + Timing.TicksPerEighth / 2 ),
						(int)(six * 0.85f), secPerEighth * 0.5 * 0.7, 0.6f );
					continue;
				}
				// grace pickup: a soft single tone just before the block stab
				Note( _time.TickToSample( h.Tick - Timing.TicksPerEighth / 2 ), (int)(six * 0.8f), 0,
					secPerEighth * 0.5 * 0.6, 0.5f );
				Stab( at, (int)(spe * 0.6f), 0.22, 1f );
				continue;
			}

			// plain stab — length varies a touch so even straight bars aren't identical
			float lenMul = 0.45f + orn.Next() * 0.35f;
			Stab( at, (int)(spe * lenMul), 0.22, 1f );
		}
	}
}