Carry the pitch LFO (vibrato) through the model#216
Conversation
|
Since LFOs with pitch are mostly used in combination with the modulation wheel ("via"-modulation), could this cause some unwanted "wobbly" sounds? |
|
Good catch — it turned out to matter, for DLS. DLS defines the connection from the low frequency oscillator to the pitch twice: once without a controller, which is the vibrato that always sounds, and once controlled by the modulation wheel, which is the amount the wheel can dial in. My lookup matched only the source and the destination, so it returned whichever of the two came first in the file — the wheel amount could therefore have been applied as a permanently sounding vibrato, exactly the wobble you describe. Both are present for nearly every region of the Roland GS set which ships with macOS:
The wheel amount is roughly five to ten times the always sounding one, so converting the wrong one would have been plainly audible. Fixed in 34d4606: the vibrato is now read from an uncontrolled connection only. The converted output of that set does not change, because the uncontrolled connection happens to come first there — so this is a guarantee rather than a repair of something visible. The other three formats do not have the ambiguity:
The restriction is now also stated in the DLS section of |
The model had no low frequency oscillator at all, so a vibrato was dropped on every conversion even though nearly every format stores one as a dedicated, fixed-destination parameter. Adds ILfo (waveform, rate in Hertz, delay, fade-in, start phase, key sync) and ILfoModulator, mirroring the existing IEnvelope and IEnvelopeModulator pair. The modulator hangs off the sample zone as getPitchLfoModulator, next to the existing pitch envelope modulator, and its depth uses the same scaling as the latter. The depth defaults to zero, which means no modulation, so a format which does not fill it writes exactly what it wrote before. SFZ reads and writes the pitchlfo_depth, pitchlfo_freq, pitchlfo_delay and pitchlfo_fade opcodes, whose units are already the ones of the model.
The vibrato low frequency oscillator (generators vibLfoToPitch, freqVibLFO and delayVibLFO) maps directly to the pitch LFO modulator: its depth is in cent like the pitch envelope, its frequency in absolute cent like the filter cutoff, and its delay in time-cent like the envelope times, so all three reuse the existing conversions. The waveform (always a triangle in Sf2) and the fade-in have no equivalent and are not carried. A preset without a vibrato has a depth of zero and writes no generators, so existing output is unchanged.
The pitch LFO is written as an <lfo> bound to GROUP_TUNING, mirroring the existing pitch envelope writer. The frequency is stored in Hertz, the delay in seconds and the depth as the modulation amount over the same 120 semi-tone range as the pitch envelope, so all three round-trip. The oscillator the template contributes is bound to the global tuning via the mod wheel; the reader matches only GROUP_TUNING, so it is not mistaken for a vibrato. The fade-in has no equivalent and is dropped, and the waveform is mapped to the nearest of sine, square and saw.
Change log entry plus a note in the SFZ, SoundFont 2 and DecentSampler format sections.
The low frequency oscillator which modulates the pitch (connection source LFO to destination pitch) is read as the pitch LFO. The depth is a relative pitch in cent, the frequency an absolute pitch converted to Hertz with the same reference as SoundFont, and the delay is the LFO start delay in seconds. Verified against the Roland GS set which ships with macOS (gs_instruments.dls): 126 of 235 instruments carry a vibrato, all read back at plausible rates of 5.2 to 6.0 Hertz with depths of 1 to 9 cent. The two new conversions in DlsArticulation divide the raw scale by the 65536 fixed-point factor; the existing normalizeEG2ToPitch does not and therefore saturates every non-zero pitch envelope depth, but that is a separate issue and left untouched here.
34d4606 to
61af524
Compare
New "LFO (Vibrato)" column in the Pitch section, filled green with their field names for the four formats which carry it - DecentSampler, DLS, SFZ and SoundFont 2 - and red for the rest.
The format defines the connection from the low frequency oscillator to the pitch twice: once without a controller, which is the vibrato that always sounds, and once controlled by the modulation wheel, which is the amount the wheel can dial in. The lookup matched only the source and the destination, so it returned whichever of the two came first in the file and could have applied the wheel amount as a permanently sounding vibrato. The vibrato is now read from an uncontrolled connection only. In the Roland GS set that ships with macOS both connections are present for almost every region - 762 uncontrolled ones of 1 to 10 cent and 761 wheel controlled ones of 45 to 49 cent - and the converted output is unchanged, since the uncontrolled connection happened to come first.
61af524 to
d9fea87
Compare
This adds a low frequency oscillator to the model, scoped to the pitch destination (vibrato) only.
That scope is deliberate. A general LFO is hard to carry because most formats route it through a modulation matrix to arbitrary destinations, and those routings differ per device. Vibrato is the exception: nearly every format expresses it as a fixed pitch-LFO with a rate, a depth and a delay, so it does have a portable representation. This PR carries that slice and nothing more.
Model
ILfo(waveform, rate in Hertz, delay, fade-in, start phase, key sync) andILfoModulator extends IModulator, mirroring the existingIEnvelope/IEnvelopeModulatorpair. It hangs off the zone asISampleZone.getPitchLfoModulator (), next to the pitch envelope modulator, and its depth uses the sameMAX_ENVELOPE_DEPTHscaling.The depth defaults to zero (no modulation), so a format which does not fill it writes exactly what it wrote before.
DefaultSampleZoneis the only implementation ofISampleZone, so nothing else needed touching.Formats
Only formats whose LFO units are documented were wired, so no value is guessed:
pitchlfo_freq(Hz),pitchlfo_depth(cent),pitchlfo_delay,pitchlfo_fade(s)vibLfoToPitch,freqVibLFO,delayVibLFO<lfo>bound toGROUP_TUNING(rate Hz, delay s, depth over the 120 semi-tone range)Formats whose LFO rate is a device-internal value without a documented mapping to Hertz (Reason NN-XT, Logic EXS24, Yamaha YSFC, the Roland and Akai hardware samplers, Ableton, TX16Wx, ...) were left unwired rather than guessed, in the same spirit as the still-unwired
IModulator.getCurve.Verification
pitchlfolines and nothing else.gs_instruments.dls) has a vibrato on 126 of its 235 instruments; all read back at plausible rates of 5.2 to 6.0 Hz with depths of 1 to 9 cent, and carry through to SoundFont 2 and DecentSampler.Documentation
Change log entry plus a note in the SFZ, SoundFont 2, DecentSampler and DLS sections of
README-FORMATS.md. The feature-support spreadsheet is not updated yet — it would want a new "pitch LFO / vibrato" capability, and I would rather agree the column layout with you first.Noticed while doing this (not fixed here)
While decoding the DLS scale values I noticed that
DlsDetector.normalizeEG2ToPitchclamps the raw 32-bit connection value to ±1200 without first dividing by the 65536 fixed-point factor, so every non-zero DLS pitch envelope depth (and the tuning and EG2 sustain that share the helper) saturates. It is a pre-existing issue independent of this PR, the three call sites each expect a different unit, so I left it untouched here and can send a separate fix if you would like.Each format is its own commit and can be dropped independently.