From fd481fdf13fc7fe186d546cf3b035c8296f17c3e Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Mon, 17 Aug 2026 19:42:43 -0400 Subject: [PATCH] =?UTF-8?q?The=20brass=20learns=20to=20buzz=20again:=20the?= =?UTF-8?q?=20timbre=20probe=20put=20trumpet,=20trombone,=20tuba,=20muted?= =?UTF-8?q?=20trumpet,=20french=20horn=20and=20brass=20section=20down=20in?= =?UTF-8?q?=20the=20dark=20rows=20of=20the=20new=20picker=20next=20to=20th?= =?UTF-8?q?e=20woodblocks,=20which=20read=20as=20a=20layout=20bug=20until?= =?UTF-8?q?=20the=20spectrum=20said=20otherwise=20=E2=80=94=20a=20trumpet?= =?UTF-8?q?=20at=20C4=20was=20putting=2090%=20of=20its=20power=20in=20its?= =?UTF-8?q?=20fundamental=20band=20with=20the=20harmonics=20essentially=20?= =?UTF-8?q?absent,=20and=20wg=5Floop=5Fdamp,=20the=20per-instrument=20brig?= =?UTF-8?q?htness=20knob=20the=20program=20table=20is=20built=20around,=20?= =?UTF-8?q?moved=20the=20result=20by=200.1%=20between=200.00=20and=200.30.?= =?UTF-8?q?=20That=20last=20number=20is=20the=20tell:=20nothing=20was=20re?= =?UTF-8?q?circulating=20for=20a=20bore-loss=20filter=20to=20act=20on.=20T?= =?UTF-8?q?wo=20causes,=20both=20in=20the=20LIP=20branch's=20rebuild.=20Th?= =?UTF-8?q?e=20lip-resonance=20biquad=20was=20being=20ADDED=20to=20the=20l?= =?UTF-8?q?oop=20signal=20=E2=80=94=20pole=20radius=200.997=20with=20b0=20?= =?UTF-8?q?=3D=20(1-r)=20gives=20it=20unity=20gain,=20narrowband,=20centre?= =?UTF-8?q?d=20exactly=20on=20the=20played=20note,=20so=20what=20was=20lab?= =?UTF-8?q?elled=20"brass=20buzz=20brightening"=20was=20a=20fundamental=20?= =?UTF-8?q?booster;=20it=20now=20shapes=20the=20EXCITATION,=20where=20STK'?= =?UTF-8?q?s=20Brass=20puts=20it=20(lipFilter.tick(deltaPressure)).=20And?= =?UTF-8?q?=20the=20valve=20was=20starved:=20written=20out,=20into=5Fbore?= =?UTF-8?q?=20=3D=20refl*(1=20-=20lipRefl)=20+=20lipRefl*Pm,=20so=20wg=5Fr?= =?UTF-8?q?eed=5Foffset=20is=20the=20round-trip=20LOSS,=20and=20the=200.6?= =?UTF-8?q?=20inherited=20from=20the=20reed=20branch=20=E2=80=94=20where?= =?UTF-8?q?=20the=20same=20field=20means=20something=20else=20=E2=80=94=20?= =?UTF-8?q?threw=20away=2060%=20of=20the=20bore=20per=20pass=20and=20left?= =?UTF-8?q?=20the=20upper=20partials=20nowhere=20to=20live.=20Mouth=20pres?= =?UTF-8?q?sure=20was=20squashed=20too,=20(0.22=20+=200.05*wg=5Fbreath=5Fm?= =?UTF-8?q?ax)=20mapping=20the=20whole=20brass=20range=202.7-3.2=20into=20?= =?UTF-8?q?0.36-0.38,=20a=20fifth=20of=20the=20way=20into=20the=20reed=20t?= =?UTF-8?q?able's=20active=20region=20and=20well=20inside=20the=20linear?= =?UTF-8?q?=20stretch=20of=20the=20waveshapers=20below,=20so=20the=20tanh?= =?UTF-8?q?=20and=20the=20cubic=20that=20are=20supposed=20to=20make=20the?= =?UTF-8?q?=20brass=20edge=20were=20barely=20bending.=20(Its=20comment=20c?= =?UTF-8?q?alled=20wg=5Fbreath=5Fmax=20"a=20loudness=20lever=20applied=20a?= =?UTF-8?q?t=20the=20output";=20for=20LIP=20the=20field=20is=20used=20nowh?= =?UTF-8?q?ere=20else,=20so=20it=20was=20only=20ever=20the=20bore=20drive.?= =?UTF-8?q?)=20Offset=20now=200.30,=20buzz=20slope=20-3.0,=20pressure=20sc?= =?UTF-8?q?aled=20rather=20than=20squashed,=20lip=20gain=200.04=20?= =?UTF-8?q?=E2=86=92=200.01=20because=20a=20heavier=20hand=20on=20the=20ex?= =?UTF-8?q?citation=20narrows=20the=20drive=20toward=20f0=20and=20costs=20?= =?UTF-8?q?harmonics.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opening the loop back up re-awoke exactly the failure the rebuild existed to prevent, so the fix is not the tuning but the pair of guards around it. A short high-register bore driven at full pressure overblows into a higher regime: notes around D5-G5 landed twelve to sixteen semitones sharp. Raising the bore loss with pitch is physical — a real bore's wall and radiation losses climb with frequency — but on its own it never fixed it, 6 to 10 notes in 792 still jumped at every loss slope tried. Tapering the drive with pitch fixed it at every taper tried. Both are kept, at loss slope 0.70 and taper 0.80, chosen for two-dimensional margin rather than for the peak: the boundary is chaotic (0.40/0.50 fails while 0.20/0.50 and 0.70/0.50 pass), so a lucky point is worth nothing and only a neighbourhood whose neighbours also pass is worth shipping. Measured over 44 semitones x 6 seeds x 6 voices with production jitter on: upper-partial energy 27.5-32.8% against a 9.8-12.5% baseline, Bark centroid 3.33-3.44 → 3.97-4.45, worst pitch error +100 cents → within +/-25, and zero notes off by more than 50 cents where there were six. That is roughly three times the harmonic content, not the eighty per cent a real trumpet carries — the model will not reach that without overblowing, and a sine at the right pitch and a rich tone at the wrong one are both failures. bin/brass-regime-test.c asserts both properties together and exits non-zero on either, because this sits on a boundary that will not announce a regression. The change is surgical: re-measuring all 128 programs moves exactly 56-61 and leaves SynthBrass, the bowed strings, the reeds and the flute byte-identical. gm-timbre-space.json and the generated picker layout are regenerated from the new measurements, which lifts the brass from rows 13-14 to rows 11-13 — still dark, still honest, because the model still is not a trumpet. NOTE: fedac/native/src/gm_synth.c is the real file behind slab/menuband/Sources/CGMSynth/gm_synth.c, so this ships to AC Native OS as well as Menu Band. Landing it on main builds no OTA; that needs `ac-os oven`. --- fedac/native/src/gm_synth.c | 91 +++++++--- .../Sources/MenuBand/GMTimbreLayout.swift | 34 ++-- slab/menuband/bin/brass-regime-test.c | 163 ++++++++++++++++++ slab/menuband/bin/gm-timbre-space.json | 12 +- 4 files changed, 255 insertions(+), 45 deletions(-) create mode 100644 slab/menuband/bin/brass-regime-test.c diff --git a/fedac/native/src/gm_synth.c b/fedac/native/src/gm_synth.c index 9f9835694..9ab0a0f06 100644 --- a/fedac/native/src/gm_synth.c +++ b/fedac/native/src/gm_synth.c @@ -1567,16 +1567,24 @@ static void gm_waveguide_init(GMVoice *v, const GMProgramParams *p, double f0, v->wg_lip_b2 = -(1.0 - r); v->wg_lip_x1 = v->wg_lip_x2 = v->wg_lip_y1 = v->wg_lip_y2 = 0.0; // Lip-formant drive: the program's wg_lip_gain (6-10, sized for the old - // quadratic valve) is far too hot for the new reed-table topology — it - // only colours the pressure drive here, so scale it right down. + // quadratic valve) is far too hot for the reed-table topology — it only + // colours the pressure drive, so scale it right down. 0.01 rather than + // the former 0.04 because the filter now sits on the excitation: a + // heavier hand there narrows the drive toward f0 and costs harmonics + // (measured: lip gain 0.1 gives the trumpet 63% of its power above the + // fundamental, 0.25 gives 46%, 1.0 gives 26%). double lipg = p->wg_lip_gain > 0.0 ? p->wg_lip_gain : 6.0; - v->wg_lip_gain = lipg * 0.04; + v->wg_lip_gain = lipg * 0.01; // Brass embouchure reed-table (reuses the REED offset/slope fields, idle // for LIP) — same self-oscillating shape as the conical reed bore, which - // locks the bore fundamental reliably. offset ≈ rest reflection, slope ≈ - // how hard the lips buzz (jittered per voice like a real section). - v->wg_reed_offset = 0.6; - v->wg_reed_slope = voice_jitter(v, -0.85, 0.06, mul); + // locks the bore fundamental reliably. In the LIP branch the algebra is + // `into_bore = refl*(1 - lipRefl) + lipRefl*Pm`, so the offset is the + // round-trip LOSS: 0.6 threw away 60% of the bore per pass and left + // nothing for the upper partials to live in. A brass bore returns + // ~0.85-0.95, hence 0.30 here (still lossy, but a bore rather than a + // sponge), with a steeper buzz slope to match. + v->wg_reed_offset = 0.30; + v->wg_reed_slope = voice_jitter(v, -3.0, 0.06, mul); } else if (p->wg_mode == GM_WG_REED) { v->wg_reed_offset = p->wg_reed_offset; // Reed slope ≈ brightness/energy lever; jitter like FM index (f, ±6%). @@ -3049,31 +3057,70 @@ static inline double generate_waveguide_sample(GMVoice *v, double sample_rate, // adds a flared-bell BRIGHTNESS colour on top — it does NOT set pitch. double pTarget = 0.55 + 0.45 * onset; v->wg_breath += (pTarget - v->wg_breath) * 0.01; - // Mouth pressure must sit in the reed-table's ACTIVE region (~[0,1]); the - // big brass wg_breath_max (2.7-3.2) is a loudness lever (applied at the - // output), not a bore-drive — feed it in scaled so pDiff modulates the - // lip reflection instead of railing it to a DC value the DC-blocker eats. - double Pm = v->wg_breath * (0.22 + 0.05 * v->wg_breath_max) + // Mouth pressure must sit in the reed-table's ACTIVE region (~[0,1]). + // The old map, (0.22 + 0.05 * wg_breath_max), squashed the whole brass + // range 2.7-3.2 into 0.36-0.38 — a fifth of the way in, where the valve + // barely modulates and the waveshapers below stay in their linear + // stretch. (Its comment called wg_breath_max "a loudness lever applied + // at the output"; for LIP the field is used nowhere else, so it was + // only ever the bore drive.) Scaling instead puts trumpet at 0.80 and + // tuba at 0.68 — in the active region, and still ordered the way the + // programs intended. + // Pressure tapers with pitch: you do not blow a high note with the + // pressure of a pedal tone, and without the taper a short high-register + // bore driven at full pressure overblows into a higher regime. Raising + // the bore loss alone does NOT fix that (6-10 notes in 792 still jumped + // 12-16 semitones sharp at every loss slope tried); tapering the drive + // does, at every taper tried. Both are kept because the pair sits on a + // contiguous stable plateau, and the boundary is chaotic enough that a + // single lucky point is not worth trusting. + double Pm = v->wg_breath * (0.25 * v->wg_breath_max) + / (1.0 + 0.80 * (frequency / 1000.0)) * (1.0 + v->wg_noise_gain * white * 0.5); double bore_out = gm_frac_read(v->ks_buf, N, v->wg_w, delay); v->wg_loop_lp = (1.0 - v->wg_loop_damp) * bore_out + v->wg_loop_damp * v->wg_loop_lp; double refl = v->wg_loop_lp; // brass bell = open, conical-like double pDiff = Pm - refl; + // Lip resonance shapes the EXCITATION, not the loop signal — the same + // place STK's Brass puts it (`lipFilter.tick(deltaPressure)`). + // + // It used to be added to `into_bore` instead, and that was the bug. The + // biquad has pole radius 0.997 with b0 = (1-r), so its gain AT f0 is + // ~1/(1-r) x (1-r) = unity, narrowband, centred exactly on the played + // note. Adding that back into the loop signal is a fundamental booster, + // not the "buzz brightening" it was labelled. Together with a loop that + // only returned 40% per round trip it made every brass voice a sine at + // the right pitch: 90% of the trumpet's power sat in its fundamental + // band, and wg_loop_damp — the per-instrument brightness lever — had no + // measurable effect at all, because almost nothing was recirculating for + // it to damp. + double lf = v->wg_lip_b0 * pDiff + v->wg_lip_b1 * v->wg_lip_x1 + + v->wg_lip_b2 * v->wg_lip_x2 + - v->wg_lip_a1 * v->wg_lip_y1 - v->wg_lip_a2 * v->wg_lip_y2; + v->wg_lip_x2 = v->wg_lip_x1; v->wg_lip_x1 = pDiff; + v->wg_lip_y2 = v->wg_lip_y1; v->wg_lip_y1 = lf; + double drive = pDiff + v->wg_lip_gain * lf; // buzz formant on the drive // Brass lip reed-table (reuses idle REED offset/slope fields): bounded in - // [-1,1] so it can modulate energy but never the loop period. - double lipRefl = v->wg_reed_offset + v->wg_reed_slope * pDiff; + // [-1,1] so it can modulate energy but never the loop period. Written + // out, `into_bore = refl*(1 - lipRefl) + lipRefl*Pm`, so wg_reed_offset + // sets the round-trip LOSS — a brass bore wants to return ~0.85-0.95, + // i.e. an offset near 0.1, not the 0.6 inherited from the reed branch + // where the same field means something else. + // + // The loss RISES WITH PITCH, which is both physical (a real bore's wall + // and radiation losses climb with frequency) and load-bearing: with a + // constant loss, a short high-register bore driven this hard overblows + // into a higher regime — 3 notes in 264 jumped a full 12-15 semitones + // sharp, the same pitch-wander failure this branch was rebuilt to stop. + // Sloping the loss removes all of them (0/264) and costs 0.6 points of + // upper-partial energy. Clamped so the top of the range still sounds. + double loss = v->wg_reed_offset + 0.70 * (frequency / 1000.0); + if (loss > 0.75) loss = 0.75; + double lipRefl = loss + v->wg_reed_slope * drive; if (lipRefl > 1.0) lipRefl = 1.0; if (lipRefl < -1.0) lipRefl = -1.0; double into_bore = refl + lipRefl * pDiff; - // Lip-resonance biquad tracking f0: a gentle brass formant / buzz colour - // on the bore signal (brightness), low gain so it never seizes pitch. - double lf = v->wg_lip_b0 * into_bore + v->wg_lip_b1 * v->wg_lip_x1 - + v->wg_lip_b2 * v->wg_lip_x2 - - v->wg_lip_a1 * v->wg_lip_y1 - v->wg_lip_a2 * v->wg_lip_y2; - v->wg_lip_x2 = v->wg_lip_x1; v->wg_lip_x1 = into_bore; - v->wg_lip_y2 = v->wg_lip_y1; v->wg_lip_y1 = lf; - into_bore += v->wg_lip_gain * lf; // brass buzz brightening // Flared-bell brassiness: a gentle uniform cubic adds the odd-harmonic // brass edge, then tanh bounds the loop (so brightness never destabilises // pitch). Per-instrument brightness is carried by the in-loop loss filter diff --git a/slab/menuband/Sources/MenuBand/GMTimbreLayout.swift b/slab/menuband/Sources/MenuBand/GMTimbreLayout.swift index d095340eb..b80e32e4f 100644 --- a/slab/menuband/Sources/MenuBand/GMTimbreLayout.swift +++ b/slab/menuband/Sources/MenuBand/GMTimbreLayout.swift @@ -38,30 +38,30 @@ enum GMTimbreLayout { 48, 92, 40, 66, 64, 65, 67, 86, 49, 62, 52, 81, 84, 54, 111, 20, 89, 82, 63, 110, 70, 85, 124, 22, - 38, 71, 32, 97, 114, 26, 80, 2, - 101, 91, 99, 109, 39, 11, 9, 16, - 58, 61, 57, 59, 112, 47, 35, 17, - 95, 60, 56, 113, 8, 10, 13, 108, + 56, 57, 61, 97, 114, 26, 80, 2, + 101, 91, 99, 109, 38, 71, 58, 32, + 39, 59, 60, 11, 9, 16, 35, 17, + 95, 112, 47, 113, 8, 10, 13, 108, 53, 117, 125, 116, 115, 118, 12, 127, ] /// GM program → its grid slot. Inverse of `programAtSlot`. static let slotForProgram: [Int] = [ 46, 63, 95, 38, 42, 9, 37, 14, - 116, 102, 117, 101, 126, 118, 44, 45, - 103, 111, 53, 62, 79, 43, 87, 39, + 116, 108, 117, 107, 126, 118, 44, 45, + 109, 111, 53, 62, 79, 43, 87, 39, 28, 23, 93, 21, 35, 26, 18, 30, - 90, 31, 27, 110, 47, 29, 88, 100, - 66, 24, 25, 17, 32, 61, 55, 109, + 103, 31, 27, 110, 47, 29, 100, 104, + 66, 24, 25, 17, 32, 61, 55, 114, 64, 72, 57, 41, 74, 120, 77, 51, - 114, 106, 104, 107, 113, 105, 73, 82, - 68, 69, 67, 70, 52, 60, 84, 89, + 88, 89, 102, 105, 106, 90, 73, 82, + 68, 69, 67, 70, 52, 60, 84, 101, 12, 11, 20, 4, 10, 3, 13, 19, 94, 75, 81, 59, 76, 85, 71, 58, 48, 80, 49, 97, 65, 50, 56, 112, 15, 91, 16, 98, 8, 96, 40, 33, 22, 36, 54, 34, 119, 99, 83, 78, - 108, 115, 92, 124, 123, 121, 125, 0, + 113, 115, 92, 124, 123, 121, 125, 0, 7, 2, 6, 1, 86, 122, 5, 127, ] @@ -124,12 +124,12 @@ enum GMTimbreLayout { (0.0935, 0.1603), // 53 (0.2142, 0.5154), // 54 (0.2655, 0.5416), // 55 - (0.1163, 0.4784), // 56 - (0.1170, 0.4710), // 57 - (0.1198, 0.4499), // 58 - (0.1166, 0.4852), // 59 - (0.1147, 0.4754), // 60 - (0.1164, 0.4608), // 61 + (0.1733, 0.4925), // 56 + (0.1732, 0.4958), // 57 + (0.1627, 0.5363), // 58 + (0.1524, 0.5035), // 59 + (0.1485, 0.5124), // 60 + (0.1700, 0.5011), // 61 (0.2013, 0.2960), // 62 (0.1928, 0.3289), // 63 (0.2375, 0.5515), // 64 diff --git a/slab/menuband/bin/brass-regime-test.c b/slab/menuband/bin/brass-regime-test.c new file mode 100644 index 000000000..3cbc2836c --- /dev/null +++ b/slab/menuband/bin/brass-regime-test.c @@ -0,0 +1,163 @@ +// brass-regime-test.c — the two things the brass voices must do at once. +// +// A digital waveguide brass voice has a standing tension. Drive the lip valve +// hard enough to generate a real harmonic spectrum and the loop can jump to a +// higher regime — overblowing, which on a GM synth means a note lands a +// twelfth or two sharp. Damp it until that can never happen and you get a sine +// at the right pitch. gm_synth has been on both sides of that line: +// +// · before 2026-06, a quadratic valve fed straight back into the loop, and +// pitch wandered +/-300-600 cents note to note; +// · the rebuild that fixed the pitch throttled the loop to a 0.4 round-trip +// gain and added a unity-gain bandpass AT f0 into the loop signal, which +// is a fundamental booster. Trumpet came out with 90% of its power in the +// fundamental band and wg_loop_damp — the per-instrument brightness knob — +// had no measurable effect at all. +// +// So neither property alone is a passing grade, and eyeballing one note is not +// a test: the stable region has a chaotic boundary, and single lucky points sit +// right next to configurations where 6 notes in 792 jump 15 semitones. This +// sweeps every semitone of a 44-note range across several per-voice seeds with +// production jitter ON, and reports both numbers together. +// +// upper% fraction of sustained power ABOVE the fundamental. A sine is ~0. +// worstc largest pitch error in cents over the whole sweep. +// bad notes off by more than 50 cents — must be ZERO. +// +// Build + run: +// cc -O2 -I Sources/CGMSynth/include -I bin bin/brass-regime-test.c \ +// Sources/CGMSynth/gm_synth.c bin/timbre-analysis.c -lm \ +// -o /tmp/brass-regime-test +// /tmp/brass-regime-test # exits non-zero if any voice regresses +// /tmp/brass-regime-test --seeds 12 # slower, more confidence + +#include "gm_synth.h" +#include "timbre-analysis.h" + +#include +#include +#include +#include + +#define SR 48000.0 +#define NOTE_S 1.2 +#define NSAMP ((long)(SR * NOTE_S)) +#define MIDI_LO 36 +#define MIDI_HI 79 + +// Thresholds. `upper%` is not held to a real trumpet's ~80% — the model does +// not get there without overblowing — but it must stay clear of the sine the +// broken version produced (9.8-12.5%). +#define MIN_UPPER 0.15 +#define MAX_CENTS 50.0 + +static void render(int prog, double f0, float *x, uint32_t seed) { + GMVoice v; + memset(&v, 0, sizeof v); + gm_voice_init(&v, prog, f0, SR, seed); + long gate = (long)(SR * 0.9); + double env = 0, ai = 1.0 / (SR * 0.004), rd = 1.0 / (SR * 0.18); + for (long i = 0; i < NSAMP; i++) { + if (i < gate) { env += ai; if (env > 1) env = 1; } + else { env -= rd; if (env < 0) env = 0; } + double s = gm_voice_render(&v, SR, env, f0); + x[i] = isfinite(s) ? (float)s : 0.f; + } +} + +static double upper_fraction(const float *x, double f0) { + int n = 4096; + static double re[4096], im[4096]; + long a = (long)(SR * 0.4); + for (int i = 0; i < n; i++) { + double w = 0.5 * (1 - cos(2 * M_PI * i / (n - 1))); + re[i] = x[a + i] * w; im[i] = 0; + } + timbre_fft(re, im, n); + double fund = 0, tot = 0; + for (int k = 1; k < n / 2; k++) { + double hz = k * SR / n, p = re[k] * re[k] + im[k] * im[k]; + if (hz > 12000) break; + tot += p; + if (hz < f0 * 1.5) fund += p; + } + return tot > 0 ? 1.0 - fund / tot : 0.0; +} + +// Autocorrelation with octave-error protection: take the SHORTEST lag whose +// correlation is within 5% of the best. A near-sine correlates almost as well +// at twice its period, so a plain argmax invents octave drops — which is what +// the first version of this test reported before the guard existed. +static double cents_err(const float *x, double f0) { + long a = (long)(SR * 0.5), n = 8192; + double best = -2; int bl = 0; + int lo = (int)(SR / (f0 * 2.6)), hi = (int)(SR / (f0 * 0.38)); + if (lo < 20) lo = 20; + if (hi > 4000) hi = 4000; + static double r[4096]; + for (int lag = lo; lag < hi; lag++) { + double s = 0, e1 = 0, e2 = 0; + for (long i = 0; i < n; i++) { + s += x[a + i] * x[a + i + lag]; + e1 += x[a + i] * x[a + i]; + e2 += x[a + i + lag] * x[a + i + lag]; + } + r[lag] = s / (sqrt(e1 * e2) + 1e-12); + if (r[lag] > best) { best = r[lag]; bl = lag; } + } + if (best < 0.2) return 9999; // not periodic enough to judge + for (int lag = lo + 1; lag < bl; lag++) + if (r[lag] >= 0.95 * best && r[lag] > r[lag - 1] && r[lag] >= r[lag + 1]) { bl = lag; break; } + if (bl <= lo || bl >= hi - 1) return 9999; + double d = (r[bl - 1] - r[bl + 1]) / (2 * (r[bl - 1] - 2 * r[bl] + r[bl + 1]) + 1e-12); + return 1200.0 * log2((SR / ((double)bl + d)) / f0); +} + +int main(int argc, char **argv) { + int seeds = 6; + for (int i = 1; i < argc; i++) + if (!strcmp(argv[i], "--seeds") && i + 1 < argc) seeds = atoi(argv[++i]); + if (seeds < 1) seeds = 1; + + timbre_analysis_init(); + gm_synth_init(); + gm_set_organic(0.6); // production default — the jitter must not break it + + const int progs[6] = { 56, 57, 58, 59, 60, 61 }; + const char *nm[6] = { "Trumpet", "Trombone", "Tuba", "MutedTpt", "FrenchHn", "BrassSec" }; + float *x = malloc(sizeof(float) * NSAMP); + int failures = 0; + int per = (MIDI_HI - MIDI_LO + 1) * seeds; + + printf("brass regime test — %d notes x %d seeds per voice, organic 0.6\n", MIDI_HI - MIDI_LO + 1, seeds); + printf("%-9s %8s %9s %8s %6s\n", "voice", "meanUp%", "worstc", "bad", ""); + for (int i = 0; i < 6; i++) { + double sum = 0, worst = 0; + int bad = 0, cnt = 0; + for (int m = MIDI_LO; m <= MIDI_HI; m++) { + double f = 440.0 * pow(2.0, (m - 69) / 12.0); + for (int s = 1; s <= seeds; s++) { + render(progs[i], f, x, (uint32_t)(0x100 * s + m)); + sum += upper_fraction(x, f); + cnt++; + double c = cents_err(x, f); + if (fabs(c) > 9000) continue; + if (fabs(c) > MAX_CENTS) bad++; + if (fabs(c) > fabs(worst)) worst = c; + } + } + double mean = sum / cnt; + int fail = (bad > 0) || (mean < MIN_UPPER); + if (fail) failures++; + printf("%-9s %7.1f%% %+9.1f %5d/%-4d %s\n", + nm[i], 100 * mean, worst, bad, per, fail ? "FAIL" : "ok"); + } + free(x); + if (failures) { + printf("\n%d voice(s) regressed. A sine at the right pitch and a rich tone at\n" + "the wrong one are both failures; the fix has to hold both.\n", failures); + return 1; + } + printf("\nall voices hold pitch and carry harmonics.\n"); + return 0; +} diff --git a/slab/menuband/bin/gm-timbre-space.json b/slab/menuband/bin/gm-timbre-space.json index 873b3254b..8cfd98e21 100644 --- a/slab/menuband/bin/gm-timbre-space.json +++ b/slab/menuband/bin/gm-timbre-space.json @@ -68,12 +68,12 @@ { "program": 53, "brightness": 0.09353, "bite": 0.16030, "bark_centroid": 2.8897, "rise_ms": 436.667, "async_ms": 9.894, "rms": 0.332828, "silent": false }, { "program": 54, "brightness": 0.21421, "bite": 0.51542, "bark_centroid": 5.2479, "rise_ms": 32.000, "async_ms": 6.774, "rms": 0.197453, "silent": false }, { "program": 55, "brightness": 0.26552, "bite": 0.54159, "bark_centroid": 6.2504, "rise_ms": 49.333, "async_ms": 1.280, "rms": 0.087778, "silent": false }, - { "program": 56, "brightness": 0.11629, "bite": 0.47839, "bark_centroid": 3.3345, "rise_ms": 36.000, "async_ms": 10.169, "rms": 0.506781, "silent": false }, - { "program": 57, "brightness": 0.11697, "bite": 0.47103, "bark_centroid": 3.3477, "rise_ms": 40.000, "async_ms": 9.110, "rms": 0.474884, "silent": false }, - { "program": 58, "brightness": 0.11982, "bite": 0.44994, "bark_centroid": 3.4035, "rise_ms": 45.333, "async_ms": 10.011, "rms": 0.429245, "silent": false }, - { "program": 59, "brightness": 0.11663, "bite": 0.48523, "bark_centroid": 3.3411, "rise_ms": 37.333, "async_ms": 8.246, "rms": 0.442124, "silent": false }, - { "program": 60, "brightness": 0.11474, "bite": 0.47536, "bark_centroid": 3.3042, "rise_ms": 41.333, "async_ms": 7.794, "rms": 0.450296, "silent": false }, - { "program": 61, "brightness": 0.11641, "bite": 0.46083, "bark_centroid": 3.3369, "rise_ms": 41.333, "async_ms": 10.169, "rms": 0.447541, "silent": false }, + { "program": 56, "brightness": 0.17331, "bite": 0.49247, "bark_centroid": 4.4486, "rise_ms": 38.667, "async_ms": 6.642, "rms": 0.533648, "silent": false }, + { "program": 57, "brightness": 0.17321, "bite": 0.49584, "bark_centroid": 4.4467, "rise_ms": 45.333, "async_ms": 4.243, "rms": 0.516301, "silent": false }, + { "program": 58, "brightness": 0.16272, "bite": 0.53634, "bark_centroid": 4.2417, "rise_ms": 35.333, "async_ms": 3.571, "rms": 0.512080, "silent": false }, + { "program": 59, "brightness": 0.15241, "bite": 0.50346, "bark_centroid": 4.0403, "rise_ms": 36.667, "async_ms": 6.135, "rms": 0.469282, "silent": false }, + { "program": 60, "brightness": 0.14851, "bite": 0.51240, "bark_centroid": 3.9641, "rise_ms": 36.000, "async_ms": 5.416, "rms": 0.508065, "silent": false }, + { "program": 61, "brightness": 0.16999, "bite": 0.50110, "bark_centroid": 4.3838, "rise_ms": 45.333, "async_ms": 3.830, "rms": 0.479915, "silent": false }, { "program": 62, "brightness": 0.20127, "bite": 0.29597, "bark_centroid": 4.9950, "rise_ms": 483.333, "async_ms": 0.289, "rms": 0.237455, "silent": false }, { "program": 63, "brightness": 0.19282, "bite": 0.32894, "bark_centroid": 4.8298, "rise_ms": 66.667, "async_ms": 36.005, "rms": 0.286777, "silent": false }, { "program": 64, "brightness": 0.23753, "bite": 0.55147, "bark_centroid": 5.7035, "rise_ms": 24.000, "async_ms": 6.815, "rms": 0.561453, "silent": false }, -- 2.51.2