Skip to content

v0.4.8

Choose a tag to compare

@github-actions github-actions released this 25 Sep 14:28
· 82 commits to main since this release

FSVR v0.4.8: the frequency EG, the note-on damp and the channel allocator, all measured off the unit.

Four engine faults found and fixed against hardware this round, in answer to rgwan's report of pitch trouble on B056 Tech Lead, B049 Hollow, B115 Obie Strings and B120 Hit, and his separate report of a note-on click on B016 Dyno Rose and A020 Vox Morph. Three hardware sessions were recorded for it (FS1R.unlock captures/2026-09-25-6, -7 and -8), each with the gate clean and the EPROM and debug stub matching their images.

The pitch reports are fixed. The operator frequency EG is a linear ramp, not the exponential approach the engine had, and its rate is the amplitude EG's own ladder. Measured off 07_modulation_2's six feg-* segments by tracking the carrier rather than its envelope: a linear fit holds to 0.3-3.8 cents rms where the best exponential over the same points leaves 39-90, and 4.5x the swing takes 4.5x as long at a fixed attack time, which a ramp does and an approach cannot. The traverse is 23.99 and 23.98 semitones on two independent segments against 24.0 for a quartered 96-unit scale, so cal::FEG_TRAVERSE = 24.0 replaces the guessed FEG_TIME_K = 0.3 and the frequency EG comes off the INFERRED list. Four segments that were 200-2900 cents out now track to 1-5 cents; the worst envelope error on that file goes 2.16 -> 1.40 dB. Adjusting the PEG did nothing on B056 because that voice's PEG is entirely flat: the slide was always the per-operator frequency EG.

A fixed-frequency operator gets no frequency EG. Measured off 24_onset's onset-feg segment: with feg_init +25 at attack time 20 on a fixed carrier the unit holds 1007.6 Hz flat from the first frame while the engine started 939 cents sharp and took 25 ms to come down. Worst error over the sweep 939 -> 11.3 cents, mean +111.7 -> -1.2. This does not contradict the ramp measurement above, which used ratio-mode carriers: together they locate the envelope on the note's pitch word, which a fixed operator never takes. Formant operators take an absolute word too and would follow the same argument, but no capture drives one with a live frequency EG, so they still apply it rather than being changed on a guess.

Note-on damps the channel it takes instead of zeroing it. FUN_00023000 is what note-on calls once the allocator has a channel, from both note-on paths: it sets that channel's EG stage word to 4, the release stage, and writes YMP706 register 0xFC/FD, with the mask coming out of the one-hot table at EPROM 0x35B260. So the hardware releases the note it displaces. The engine did C = Chan(), which dropped a still-sounding channel to zero between two samples: 13 times in Vokodrone, 44 in AN Superarp, a step up to 0.15 of full scale. In a forced steal the largest step across the note-on goes from 1.61x the note's own body to 0.83x. The fade rate, cal::DAMP_MS, is the one constant in cal.h with no measurement behind it and is labelled as such: 23_damp was recorded for it and came back with no damp in it at all, because the unit did not take the channel that file aimed at, which is that file's designed-visible failure rather than a number to fit.

The channel allocator is round robin with a note-priority steal. Read directly out of the unit's memory rather than inferred from audio (session 6 reads 0x010287DC, 0x010287DE, 0x010289BC and 0x010288DC after every note-on). The per-channel priority word is the note number exactly. Filling an empty machine with notes 36..67 puts them on channels 1..31 and then 0, not 0..31: FUN_0000f7dc walks from the last channel allocated. With all 32 held, a 33rd note of 100 takes channel 2, which held note 37, not the oldest channel the engine used. Age is no longer consulted anywhere in allocation. On the demo songs this is not cosmetic: steals of a still-sounding channel go 13 -> 47 in Vokodrone, 44 -> 188 in AN Superarp and 0 -> 32 in Kalimba, and Vokodrone's onset high-frequency excess against the unit's own recording drops +1.6 -> +0.6 dB. Also measured there: a part's note reserve cannot oversubscribe the 32 channels, writing 32 with the others at 0+8+8 reads back as 16.

Known issue, not fixed in this release: the B016 Dyno Rose click. It is located and it is ours. 24_onset puts the unit at 1.68x its own body one control tick after note-on against the engine's 9.80x, with a control segment that has nothing tick-driven reading 1.08x, so the unit has no such discontinuity anywhere. Muting part-1 operators one at a time drops the engine's ratio from 4.41x to 1.77x on operator 2 alone, and switching the parts' insertion switch off removes the step entirely while zeroing the reverb send changes nothing: it is the insertion path. What does not add up yet is that B016's reverb, variation and insertion types all read 0, which is pass-through in both blocks. Deferred at rgwan's direction, since the effect blocks are to be replaced by a VOP3 model. A020 Vox Morph measures clean. Also still open and unrelated: four demo songs carry 4-10 dB of onset high-frequency energy the unit does not, unmoved by every fix above.

Tooling in this line: the render regression runs off a portable renderer so the gate works away from Windows; tools/fit_damp.py, tools/fit_onset.py (with a --check regression gate for the fixed-operator frequency EG) and the 23_damp and 24_onset request files are new; the engine selftest gained checks for the frequency EG's constant rate, the note-on damp and the allocator's measured channel numbers, each verified to fail on a deliberately broken build. A pre-existing bug where "selftest: ok" printed mid-function, hiding every check after it from the exit code, is fixed.

This version was previously erroneously released as v0.5.0.