Part of the MusicGen class, implements the backing horn section rendering. It lays out chord tones across a stereo spread, applies expression and voicing, and emits stabs, arpeggios, pairs and grace notes based on probabilistic choices and timing.
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 );
}
}
}