From 070a88a3a9ab23346b3be68f5d59a88660ec64a1 Mon Sep 17 00:00:00 2001 From: Douglas Carmichael Date: Tue, 21 Jul 2026 11:23:47 -0400 Subject: [PATCH 1/8] Carry the pitch LFO (vibrato) through the model 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. --- .../convertwithmoss/core/model/ILfo.java | 122 ++++++++++++++ .../core/model/ILfoModulator.java | 29 ++++ .../core/model/ISampleZone.java | 9 ++ .../core/model/enumeration/LfoWaveform.java | 27 ++++ .../core/model/implementation/DefaultLfo.java | 152 ++++++++++++++++++ .../implementation/DefaultLfoModulator.java | 75 +++++++++ .../implementation/DefaultSampleZone.java | 11 ++ .../format/sfz/SfzCreator.java | 30 ++++ .../format/sfz/SfzDetector.java | 18 +++ .../convertwithmoss/format/sfz/SfzOpcode.java | 9 ++ 10 files changed, 482 insertions(+) create mode 100644 src/main/java/de/mossgrabers/convertwithmoss/core/model/ILfo.java create mode 100644 src/main/java/de/mossgrabers/convertwithmoss/core/model/ILfoModulator.java create mode 100644 src/main/java/de/mossgrabers/convertwithmoss/core/model/enumeration/LfoWaveform.java create mode 100644 src/main/java/de/mossgrabers/convertwithmoss/core/model/implementation/DefaultLfo.java create mode 100644 src/main/java/de/mossgrabers/convertwithmoss/core/model/implementation/DefaultLfoModulator.java diff --git a/src/main/java/de/mossgrabers/convertwithmoss/core/model/ILfo.java b/src/main/java/de/mossgrabers/convertwithmoss/core/model/ILfo.java new file mode 100644 index 00000000..f493cb6d --- /dev/null +++ b/src/main/java/de/mossgrabers/convertwithmoss/core/model/ILfo.java @@ -0,0 +1,122 @@ +// Written by Jürgen Moßgraber - mossgrabers.de +// (c) 2019-2026 +// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt + +package de.mossgrabers.convertwithmoss.core.model; + +import de.mossgrabers.convertwithmoss.core.model.enumeration.LfoWaveform; + + +/** + * Interface to a low frequency oscillator. All times are given in seconds and the rate is given in + * Hertz, so that the values are comparable across formats. A format which stores a tempo + * synchronized rate needs to convert it, since the tempo is not part of a multi-sample. + * + * @author Jürgen Moßgraber + */ +public interface ILfo +{ + /** + * Get the waveform. + * + * @return The waveform + */ + LfoWaveform getWaveform (); + + + /** + * Set the waveform. + * + * @param waveform The waveform + */ + void setWaveform (LfoWaveform waveform); + + + /** + * Get the rate. + * + * @return The rate in Hertz or -1 if not set + */ + double getRate (); + + + /** + * Set the rate. + * + * @param rate The rate in Hertz + */ + void setRate (double rate); + + + /** + * Get the delay, which is the time before the modulation starts. + * + * @return The delay in seconds or -1 if not set + */ + double getDelay (); + + + /** + * Set the delay, which is the time before the modulation starts. + * + * @param delay The delay in seconds + */ + void setDelay (double delay); + + + /** + * Get the fade-in, which is the time it takes to reach the full modulation depth. + * + * @return The fade-in in seconds or -1 if not set + */ + double getFadeIn (); + + + /** + * Set the fade-in, which is the time it takes to reach the full modulation depth. + * + * @param fadeIn The fade-in in seconds + */ + void setFadeIn (double fadeIn); + + + /** + * Get the phase at which the waveform starts. + * + * @return The start phase in the range of [0..1] or -1 if not set + */ + double getStartPhase (); + + + /** + * Set the phase at which the waveform starts. + * + * @param startPhase The start phase in the range of [0..1] + */ + void setStartPhase (double startPhase); + + + /** + * Check if the oscillator restarts when a note is triggered. If it does not, it runs freely in + * the background. + * + * @return True if it restarts on a note + */ + boolean isKeySync (); + + + /** + * Set if the oscillator restarts when a note is triggered. + * + * @param isKeySync True if it restarts on a note + */ + void setKeySync (boolean isKeySync); + + + /** + * Check if any value was set. A rate needs to be present for the oscillator to be of any use. + * + * @return True if a rate was set + */ + boolean isSet (); +} diff --git a/src/main/java/de/mossgrabers/convertwithmoss/core/model/ILfoModulator.java b/src/main/java/de/mossgrabers/convertwithmoss/core/model/ILfoModulator.java new file mode 100644 index 00000000..3b265afc --- /dev/null +++ b/src/main/java/de/mossgrabers/convertwithmoss/core/model/ILfoModulator.java @@ -0,0 +1,29 @@ +// Written by Jürgen Moßgraber - mossgrabers.de +// (c) 2019-2026 +// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt + +package de.mossgrabers.convertwithmoss.core.model; + +/** + * Interface to a low frequency oscillator modulator. The modulation depth of the pitch modulation + * maps to -4800..4800 cent, which is the same range as the one of the pitch envelope modulator. + * + * @author Jürgen Moßgraber + */ +public interface ILfoModulator extends IModulator +{ + /** + * Get the modulation source. + * + * @return The modulation source, never null + */ + ILfo getSource (); + + + /** + * Set the modulation source. + * + * @param source The modulation source + */ + void setSource (ILfo source); +} diff --git a/src/main/java/de/mossgrabers/convertwithmoss/core/model/ISampleZone.java b/src/main/java/de/mossgrabers/convertwithmoss/core/model/ISampleZone.java index 1fed6aeb..100430f8 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/core/model/ISampleZone.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/core/model/ISampleZone.java @@ -485,6 +485,15 @@ public interface ISampleZone IEnvelopeModulator getPitchEnvelopeModulator (); + /** + * Get the low frequency oscillator modulator for the pitch, which is commonly called vibrato. A + * depth of zero means that there is no vibrato. + * + * @return The modulator, never null + */ + ILfoModulator getPitchLfoModulator (); + + /** * Get pitch bend up value. * diff --git a/src/main/java/de/mossgrabers/convertwithmoss/core/model/enumeration/LfoWaveform.java b/src/main/java/de/mossgrabers/convertwithmoss/core/model/enumeration/LfoWaveform.java new file mode 100644 index 00000000..cce37cb0 --- /dev/null +++ b/src/main/java/de/mossgrabers/convertwithmoss/core/model/enumeration/LfoWaveform.java @@ -0,0 +1,27 @@ +// Written by Jürgen Moßgraber - mossgrabers.de +// (c) 2019-2026 +// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt + +package de.mossgrabers.convertwithmoss.core.model.enumeration; + +/** + * Different waveforms of a low frequency oscillator. Only the shapes which are present in most + * formats are listed, a format which knows further shapes needs to map them to the closest one. + * + * @author Jürgen Moßgraber + */ +public enum LfoWaveform +{ + /** A sine wave. */ + SINE, + /** A triangle wave. */ + TRIANGLE, + /** A square wave. */ + SQUARE, + /** A rising saw-tooth wave. */ + SAWTOOTH_UP, + /** A falling saw-tooth wave. */ + SAWTOOTH_DOWN, + /** A random (sample and hold) wave. */ + RANDOM, +} diff --git a/src/main/java/de/mossgrabers/convertwithmoss/core/model/implementation/DefaultLfo.java b/src/main/java/de/mossgrabers/convertwithmoss/core/model/implementation/DefaultLfo.java new file mode 100644 index 00000000..7f638e44 --- /dev/null +++ b/src/main/java/de/mossgrabers/convertwithmoss/core/model/implementation/DefaultLfo.java @@ -0,0 +1,152 @@ +// Written by Jürgen Moßgraber - mossgrabers.de +// (c) 2019-2026 +// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt + +package de.mossgrabers.convertwithmoss.core.model.implementation; + +import java.util.Objects; + +import de.mossgrabers.convertwithmoss.core.model.ILfo; +import de.mossgrabers.convertwithmoss.core.model.enumeration.LfoWaveform; + + +/** + * Default implementation of a low frequency oscillator. All values are unset by default, which + * means that a format which does not fill them writes nothing. + * + * @author Jürgen Moßgraber + */ +public class DefaultLfo implements ILfo +{ + private LfoWaveform waveform = LfoWaveform.TRIANGLE; + private double rate = -1; + private double delay = -1; + private double fadeIn = -1; + private double startPhase = -1; + private boolean isKeySync = false; + + + /** {@inheritDoc} */ + @Override + public LfoWaveform getWaveform () + { + return this.waveform; + } + + + /** {@inheritDoc} */ + @Override + public void setWaveform (final LfoWaveform waveform) + { + this.waveform = waveform; + } + + + /** {@inheritDoc} */ + @Override + public double getRate () + { + return this.rate; + } + + + /** {@inheritDoc} */ + @Override + public void setRate (final double rate) + { + this.rate = rate; + } + + + /** {@inheritDoc} */ + @Override + public double getDelay () + { + return this.delay; + } + + + /** {@inheritDoc} */ + @Override + public void setDelay (final double delay) + { + this.delay = delay; + } + + + /** {@inheritDoc} */ + @Override + public double getFadeIn () + { + return this.fadeIn; + } + + + /** {@inheritDoc} */ + @Override + public void setFadeIn (final double fadeIn) + { + this.fadeIn = fadeIn; + } + + + /** {@inheritDoc} */ + @Override + public double getStartPhase () + { + return this.startPhase; + } + + + /** {@inheritDoc} */ + @Override + public void setStartPhase (final double startPhase) + { + this.startPhase = startPhase; + } + + + /** {@inheritDoc} */ + @Override + public boolean isKeySync () + { + return this.isKeySync; + } + + + /** {@inheritDoc} */ + @Override + public void setKeySync (final boolean isKeySync) + { + this.isKeySync = isKeySync; + } + + + /** {@inheritDoc} */ + @Override + public boolean isSet () + { + return this.rate >= 0; + } + + + /** {@inheritDoc} */ + @Override + public int hashCode () + { + return Objects.hash (Double.valueOf (this.delay), Double.valueOf (this.fadeIn), Boolean.valueOf (this.isKeySync), Double.valueOf (this.rate), Double.valueOf (this.startPhase), this.waveform); + } + + + /** {@inheritDoc} */ + @Override + public boolean equals (final Object obj) + { + if (this == obj) + return true; + if (obj == null || this.getClass () != obj.getClass ()) + return false; + final DefaultLfo other = (DefaultLfo) obj; + return Double.doubleToLongBits (this.delay) == Double.doubleToLongBits (other.delay) && Double.doubleToLongBits (this.fadeIn) == Double.doubleToLongBits (other.fadeIn) && this.isKeySync == other.isKeySync && Double.doubleToLongBits (this.rate) == Double.doubleToLongBits (other.rate) && Double.doubleToLongBits (this.startPhase) == Double.doubleToLongBits (other.startPhase) && this.waveform == other.waveform; + } +} diff --git a/src/main/java/de/mossgrabers/convertwithmoss/core/model/implementation/DefaultLfoModulator.java b/src/main/java/de/mossgrabers/convertwithmoss/core/model/implementation/DefaultLfoModulator.java new file mode 100644 index 00000000..44ed59a7 --- /dev/null +++ b/src/main/java/de/mossgrabers/convertwithmoss/core/model/implementation/DefaultLfoModulator.java @@ -0,0 +1,75 @@ +// Written by Jürgen Moßgraber - mossgrabers.de +// (c) 2019-2026 +// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt + +package de.mossgrabers.convertwithmoss.core.model.implementation; + +import java.util.Objects; + +import de.mossgrabers.convertwithmoss.core.model.ILfo; +import de.mossgrabers.convertwithmoss.core.model.ILfoModulator; + + +/** + * Default implementation of a low frequency oscillator modulator. + * + * @author Jürgen Moßgraber + */ +public class DefaultLfoModulator extends DefaultModulator implements ILfoModulator +{ + private ILfo source = new DefaultLfo (); + + + /** + * Constructor. + * + * @param depth The modulation depth in the range of [-1,1]. A depth of 0 means that there is no + * modulation, which is the default for all formats which do not support it + */ + public DefaultLfoModulator (final double depth) + { + super (depth); + } + + + /** {@inheritDoc} */ + @Override + public ILfo getSource () + { + if (this.source == null) + this.source = new DefaultLfo (); + return this.source; + } + + + /** {@inheritDoc} */ + @Override + public void setSource (final ILfo source) + { + this.source = source; + } + + + /** {@inheritDoc} */ + @Override + public int hashCode () + { + final int prime = 31; + int result = super.hashCode (); + result = prime * result + Objects.hash (this.source); + return result; + } + + + /** {@inheritDoc} */ + @Override + public boolean equals (final Object obj) + { + if (this == obj) + return true; + if (!super.equals (obj) || this.getClass () != obj.getClass ()) + return false; + final DefaultLfoModulator other = (DefaultLfoModulator) obj; + return Objects.equals (this.source, other.source); + } +} diff --git a/src/main/java/de/mossgrabers/convertwithmoss/core/model/implementation/DefaultSampleZone.java b/src/main/java/de/mossgrabers/convertwithmoss/core/model/implementation/DefaultSampleZone.java index b2ddf4d4..c29b7026 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/core/model/implementation/DefaultSampleZone.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/core/model/implementation/DefaultSampleZone.java @@ -10,6 +10,7 @@ import de.mossgrabers.convertwithmoss.core.model.IEnvelopeModulator; import de.mossgrabers.convertwithmoss.core.model.IFilter; +import de.mossgrabers.convertwithmoss.core.model.ILfoModulator; import de.mossgrabers.convertwithmoss.core.model.IModulator; import de.mossgrabers.convertwithmoss.core.model.ISampleData; import de.mossgrabers.convertwithmoss.core.model.ISampleLoop; @@ -54,6 +55,7 @@ public class DefaultSampleZone implements ISampleZone protected IModulator amplitudeVelocityModulator = new DefaultModulator (1); protected IEnvelopeModulator amplitudeEnvelopeModulator = new DefaultEnvelopeModulator (1); protected IEnvelopeModulator pitchModulator = new DefaultEnvelopeModulator (0); + protected ILfoModulator pitchLfoModulator = new DefaultLfoModulator (0); protected IFilter filter = null; protected List loops = new ArrayList<> (1); @@ -567,6 +569,14 @@ public IEnvelopeModulator getPitchEnvelopeModulator () } + /** {@inheritDoc} */ + @Override + public ILfoModulator getPitchLfoModulator () + { + return this.pitchLfoModulator; + } + + /** {@inheritDoc} */ @Override public Optional getFilter () @@ -614,6 +624,7 @@ public void fillMetadata (final ISampleZone other) this.amplitudeVelocityModulator = other.getAmplitudeVelocityModulator (); this.amplitudeEnvelopeModulator = other.getAmplitudeEnvelopeModulator (); this.pitchModulator = other.getPitchEnvelopeModulator (); + this.pitchLfoModulator = other.getPitchLfoModulator (); final Optional filterOpt = other.getFilter (); this.filter = filterOpt.isPresent () ? filterOpt.get () : null; diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/sfz/SfzCreator.java b/src/main/java/de/mossgrabers/convertwithmoss/format/sfz/SfzCreator.java index 550b24ec..d8fbce11 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/sfz/SfzCreator.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/sfz/SfzCreator.java @@ -28,6 +28,8 @@ import de.mossgrabers.convertwithmoss.core.model.IEnvelopeModulator; import de.mossgrabers.convertwithmoss.core.model.IFilter; import de.mossgrabers.convertwithmoss.core.model.IGroup; +import de.mossgrabers.convertwithmoss.core.model.ILfo; +import de.mossgrabers.convertwithmoss.core.model.ILfoModulator; import de.mossgrabers.convertwithmoss.core.model.IMetadata; import de.mossgrabers.convertwithmoss.core.model.ISampleData; import de.mossgrabers.convertwithmoss.core.model.ISampleLoop; @@ -373,6 +375,24 @@ private void createSample (final String safeSampleFolderName, final StringBuilde buffer.append (envelopeStr).append (LINE_FEED); } + // ----------------------------------------------------------- + // Pitch LFO (vibrato) + + final ILfoModulator pitchLfoModulator = zone.getPitchLfoModulator (); + final double lfoDepth = pitchLfoModulator.getDepth (); + if (lfoDepth != 0) + { + final StringBuilder lfoStr = new StringBuilder (); + lfoStr.append (SfzOpcode.PITCHLFO_DEPTH).append ('=').append ((int) Math.round (lfoDepth * IEnvelope.MAX_ENVELOPE_DEPTH)); + + final ILfo pitchLfo = pitchLfoModulator.getSource (); + addLfoTimeAttribute (lfoStr, SfzOpcode.PITCHLFO_FREQ, pitchLfo.getRate ()); + addLfoTimeAttribute (lfoStr, SfzOpcode.PITCHLFO_DELAY, pitchLfo.getDelay ()); + addLfoTimeAttribute (lfoStr, SfzOpcode.PITCHLFO_FADE, pitchLfo.getFadeIn ()); + + buffer.append (lfoStr).append (LINE_FEED); + } + // ----------------------------------------------------------- // Sample Loop @@ -595,6 +615,16 @@ private static void addEnvelopeLevelAttribute (final StringBuilder sb, final Str } + private static void addLfoTimeAttribute (final StringBuilder sb, final String opcode, final double value) + { + if (value < 0) + return; + if (!sb.isEmpty ()) + sb.append (' '); + sb.append (opcode).append ('=').append (formatAsFloat (value)); + } + + private static void addSlopeAttribute (final StringBuilder sb, final String opcode, final double value) { if (value == 0) diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/sfz/SfzDetector.java b/src/main/java/de/mossgrabers/convertwithmoss/format/sfz/SfzDetector.java index de17fb4f..d90a4d7f 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/sfz/SfzDetector.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/sfz/SfzDetector.java @@ -27,6 +27,8 @@ import de.mossgrabers.convertwithmoss.core.model.IEnvelopeModulator; import de.mossgrabers.convertwithmoss.core.model.IFilter; import de.mossgrabers.convertwithmoss.core.model.IGroup; +import de.mossgrabers.convertwithmoss.core.model.ILfo; +import de.mossgrabers.convertwithmoss.core.model.ILfoModulator; import de.mossgrabers.convertwithmoss.core.model.ISampleData; import de.mossgrabers.convertwithmoss.core.model.ISampleLoop; import de.mossgrabers.convertwithmoss.core.model.ISampleZone; @@ -500,6 +502,22 @@ private void parseRegion (final ISampleZone sampleMetadata) pitchEnvelope.setDecaySlope (this.getDoubleValue (SfzOpcode.PITCHEG_DECAY_SHAPE, 0) / 10.0); pitchEnvelope.setReleaseSlope (this.getDoubleValue (SfzOpcode.PITCHEG_RELEASE_SHAPE, 0) / 10.0); + // ----------------------------------------------------------- + // Pitch LFO (vibrato) + + // Without a depth there is no modulation, therefore the oscillator is not read at all + final double lfoDepth = this.getDoubleValue (SfzOpcode.PITCHLFO_DEPTH, 0); + if (lfoDepth != 0) + { + final ILfoModulator pitchLfoModulator = sampleMetadata.getPitchLfoModulator (); + pitchLfoModulator.setDepth (lfoDepth / IEnvelope.MAX_ENVELOPE_DEPTH); + + final ILfo pitchLfo = pitchLfoModulator.getSource (); + pitchLfo.setRate (this.getDoubleValue (SfzOpcode.PITCHLFO_FREQ, -1)); + pitchLfo.setDelay (this.getDoubleValue (SfzOpcode.PITCHLFO_DELAY, -1)); + pitchLfo.setFadeIn (this.getDoubleValue (SfzOpcode.PITCHLFO_FADE, -1)); + } + // ----------------------------------------------------------- // Volume diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/sfz/SfzOpcode.java b/src/main/java/de/mossgrabers/convertwithmoss/format/sfz/SfzOpcode.java index 7cd5b219..cf8571e2 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/sfz/SfzOpcode.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/sfz/SfzOpcode.java @@ -261,6 +261,15 @@ public class SfzOpcode /** ARIA. The pitch EG release slope time. */ public static final String PITCHEG_RELEASE_SHAPE = "pitcheg_release_shape"; + /** SFZ v1. The pitch LFO (vibrato) frequency in Hertz. */ + public static final String PITCHLFO_FREQ = "pitchlfo_freq"; + /** SFZ v1. The pitch LFO (vibrato) depth in cent. */ + public static final String PITCHLFO_DEPTH = "pitchlfo_depth"; + /** SFZ v1. The pitch LFO (vibrato) delay time in seconds. */ + public static final String PITCHLFO_DELAY = "pitchlfo_delay"; + /** SFZ v1. The pitch LFO (vibrato) fade-in time in seconds. */ + public static final String PITCHLFO_FADE = "pitchlfo_fade"; + /** * Private constructor for utility class. From 807b9c6e5e3ead7f90fe8455ab5d250d06f59e54 Mon Sep 17 00:00:00 2001 From: Douglas Carmichael Date: Fri, 24 Jul 2026 07:03:27 -0400 Subject: [PATCH 2/8] SoundFont 2: carry the vibrato LFO as the pitch LFO 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. --- .../convertwithmoss/file/sf2/Generator.java | 8 +++++++ .../format/sf2/Sf2Creator.java | 22 +++++++++++++++++++ .../format/sf2/Sf2Detector.java | 15 +++++++++++++ 3 files changed, 45 insertions(+) diff --git a/src/main/java/de/mossgrabers/convertwithmoss/file/sf2/Generator.java b/src/main/java/de/mossgrabers/convertwithmoss/file/sf2/Generator.java index 5fc293dd..cc5665e9 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/file/sf2/Generator.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/file/sf2/Generator.java @@ -23,6 +23,9 @@ public class Generator /** The ID of the end loop offset. */ public static final int END_LOOP_ADDRS_OFFSET = 3; + /** The ID of the vibrato low frequency oscillator to pitch generator. */ + public static final int VIB_LFO_TO_PITCH = 6; + /** The ID of the modulation envelope to pitch generator. */ public static final int MOD_ENV_TO_PITCH = 7; @@ -37,6 +40,11 @@ public class Generator /** The ID of the panning generator. */ public static final int PANNING = 17; + /** The ID of the vibrato low frequency oscillator delay generator (in time-cents). */ + public static final int DELAY_VIB_LFO = 23; + /** The ID of the vibrato low frequency oscillator frequency generator (in absolute cents). */ + public static final int FREQ_VIB_LFO = 24; + /** The ID of the modulation envelope delay generator. */ public static final int MOD_ENV_DELAY = 25; /** The ID of the modulation envelope attack generator. */ diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/sf2/Sf2Creator.java b/src/main/java/de/mossgrabers/convertwithmoss/format/sf2/Sf2Creator.java index c24056b9..048968ee 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/sf2/Sf2Creator.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/sf2/Sf2Creator.java @@ -22,6 +22,8 @@ import de.mossgrabers.convertwithmoss.core.model.IEnvelope; import de.mossgrabers.convertwithmoss.core.model.IEnvelopeModulator; import de.mossgrabers.convertwithmoss.core.model.IFilter; +import de.mossgrabers.convertwithmoss.core.model.ILfo; +import de.mossgrabers.convertwithmoss.core.model.ILfoModulator; import de.mossgrabers.convertwithmoss.core.model.IGroup; import de.mossgrabers.convertwithmoss.core.model.IMetadata; import de.mossgrabers.convertwithmoss.core.model.ISampleData; @@ -421,6 +423,26 @@ else if (sampleType == Sf2SampleDescriptor.RIGHT) setEnvelopeKeyTracking (instrumentZone, Generator.KEYNUM_TO_MOD_ENV_HOLD, Generator.KEYNUM_TO_MOD_ENV_DECAY, pitchEnvelope.getTimeKeyTracking ()); } + // Set the vibrato low frequency oscillator from the pitch modulation. The depth is given in + // cent like the pitch envelope, the fade-in and the waveform have no equivalent in Sf2. + final ILfoModulator pitchLfoModulator = sampleZone.getPitchLfoModulator (); + final double vibLfoDepth = pitchLfoModulator.getDepth (); + if (vibLfoDepth != 0) + { + instrumentZone.addSignedGenerator (Generator.VIB_LFO_TO_PITCH, (int) Math.round (vibLfoDepth * IEnvelope.MAX_ENVELOPE_DEPTH)); + final ILfo pitchLfo = pitchLfoModulator.getSource (); + final double rate = pitchLfo.getRate (); + if (rate > 0) + { + // The frequency is stored in absolute cents, see the filter cutoff below + final double frequencyCents = Math.log (rate / 8.176) * 1200.0 / Math.log (2); + instrumentZone.addSignedGenerator (Generator.FREQ_VIB_LFO, (int) Math.round (frequencyCents)); + } + final double delay = pitchLfo.getDelay (); + if (delay >= 0) + instrumentZone.addSignedGenerator (Generator.DELAY_VIB_LFO, convertEnvelopeTime (delay)); + } + // Filter settings final Optional filterOpt = sampleZone.getFilter (); if (filterOpt.isPresent ()) diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/sf2/Sf2Detector.java b/src/main/java/de/mossgrabers/convertwithmoss/format/sf2/Sf2Detector.java index 88848e9d..8158fc4b 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/sf2/Sf2Detector.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/sf2/Sf2Detector.java @@ -19,6 +19,8 @@ import de.mossgrabers.convertwithmoss.core.model.IEnvelope; import de.mossgrabers.convertwithmoss.core.model.IEnvelopeModulator; import de.mossgrabers.convertwithmoss.core.model.IFilter; +import de.mossgrabers.convertwithmoss.core.model.ILfo; +import de.mossgrabers.convertwithmoss.core.model.ILfoModulator; import de.mossgrabers.convertwithmoss.core.model.IGroup; import de.mossgrabers.convertwithmoss.core.model.IMetadata; import de.mossgrabers.convertwithmoss.core.model.ISampleData; @@ -849,6 +851,19 @@ private static Optional createSampleZone (final Sf2SampleDescriptor pitchEnvelope.setTimeKeyTracking (convertEnvelopeKeyTracking (generators.getSignedValue (Generator.KEYNUM_TO_MOD_ENV_DECAY), generators.getSignedValue (Generator.KEYNUM_TO_MOD_ENV_HOLD))); } + // The vibrato low frequency oscillator maps to the pitch modulation. Its waveform is + // always a triangle and the depth is given in cent, like the pitch envelope. + final ILfoModulator pitchLfoModulator = zone.getPitchLfoModulator (); + final int vibLfoDepth = generators.getSignedValue (Generator.VIB_LFO_TO_PITCH).intValue (); + pitchLfoModulator.setDepth (vibLfoDepth / (double) IEnvelope.MAX_ENVELOPE_DEPTH); + if (vibLfoDepth != 0) + { + final ILfo pitchLfo = pitchLfoModulator.getSource (); + // The frequency is stored in absolute cents, see the filter cutoff above + pitchLfo.setRate (8.176 * Math.pow (2, generators.getSignedValue (Generator.FREQ_VIB_LFO).doubleValue () / 1200.0)); + pitchLfo.setDelay (convertEnvelopeTime (generators.getSignedValue (Generator.DELAY_VIB_LFO))); + } + return Optional.of (zone); } catch (final IOException _) From 1469530c5bec8035aa6b2aca42905b8cd74ef647 Mon Sep 17 00:00:00 2001 From: Douglas Carmichael Date: Fri, 24 Jul 2026 07:12:27 -0400 Subject: [PATCH 3/8] DecentSampler: read and write the pitch LFO (vibrato) The pitch LFO is written as an 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. --- .../decentsampler/DecentSamplerCreator.java | 55 +++++++++++++++++++ .../decentsampler/DecentSamplerDetector.java | 54 ++++++++++++++++++ .../decentsampler/DecentSamplerTag.java | 10 ++++ 3 files changed, 119 insertions(+) diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/decentsampler/DecentSamplerCreator.java b/src/main/java/de/mossgrabers/convertwithmoss/format/decentsampler/DecentSamplerCreator.java index 6c0999a3..b953da9c 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/decentsampler/DecentSamplerCreator.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/decentsampler/DecentSamplerCreator.java @@ -37,9 +37,12 @@ import de.mossgrabers.convertwithmoss.core.model.IEnvelopeModulator; import de.mossgrabers.convertwithmoss.core.model.IFilter; import de.mossgrabers.convertwithmoss.core.model.IGroup; +import de.mossgrabers.convertwithmoss.core.model.ILfo; +import de.mossgrabers.convertwithmoss.core.model.ILfoModulator; import de.mossgrabers.convertwithmoss.core.model.ISampleLoop; import de.mossgrabers.convertwithmoss.core.model.ISampleZone; import de.mossgrabers.convertwithmoss.core.model.enumeration.FilterType; +import de.mossgrabers.convertwithmoss.core.model.enumeration.LfoWaveform; import de.mossgrabers.convertwithmoss.core.model.enumeration.PlayLogic; import de.mossgrabers.convertwithmoss.core.model.enumeration.TriggerType; import de.mossgrabers.convertwithmoss.core.model.implementation.DefaultFilter; @@ -342,7 +345,10 @@ else if (ampEnvParameterLevel == ParameterLevel.INSTRUMENT && groupIndex == 0 && this.createFilter (document, modulatorsElement, multisampleSource, groupElement, groupIndex); if (!zones.isEmpty ()) + { createPitchModulator (document, modulatorsElement, zones.get (0).getPitchEnvelopeModulator (), groupIndex); + createPitchLfoModulator (document, modulatorsElement, zones.get (0).getPitchLfoModulator (), groupIndex); + } } this.applyPolyphony (document, multisampleElement, groupsElement, multisampleSource); @@ -534,6 +540,55 @@ private void createFilter (final Document document, final Element modulatorsElem } + private static void createPitchLfoModulator (final Document document, final Element modulatorsElement, final ILfoModulator pitchLfoModulator, final int groupIndex) + { + final double lfoDepth = pitchLfoModulator.getDepth (); + if (lfoDepth == 0) + return; + + final ILfo pitchLfo = pitchLfoModulator.getSource (); + + final Element lfoElement = XMLUtils.addElement (document, modulatorsElement, DecentSamplerTag.LFO); + lfoElement.setAttribute (DecentSamplerTag.LFO_SHAPE, toLfoShape (pitchLfo.getWaveform ())); + // The modulation depth is applied through the binding range below, like the pitch envelope. + // A vibrato depth is small in relation to the full range, so more digits are needed here. + XMLUtils.setDoubleAttribute (lfoElement, DecentSamplerTag.MOD_AMOUNT, Math.abs (lfoDepth), 5); + final double rate = pitchLfo.getRate (); + if (rate > 0) + { + lfoElement.setAttribute (DecentSamplerTag.LFO_FREQUENCY_FORMAT, "hz"); + XMLUtils.setDoubleAttribute (lfoElement, DecentSamplerTag.LFO_FREQUENCY, rate, 3); + } + final double delay = pitchLfo.getDelay (); + if (delay > 0) + XMLUtils.setDoubleAttribute (lfoElement, DecentSamplerTag.LFO_DELAY_TIME, delay, 3); + + final Element bindingElement = XMLUtils.addElement (document, lfoElement, DecentSamplerTag.BINDING); + bindingElement.setAttribute ("type", "amp"); + bindingElement.setAttribute ("level", "group"); + bindingElement.setAttribute ("groupIndex", Integer.toString (groupIndex)); + bindingElement.setAttribute ("parameter", "GROUP_TUNING"); + bindingElement.setAttribute ("translation", "linear"); + // Unit are semi-tones; the oscillator is bipolar, so the full range is applied symmetrically + bindingElement.setAttribute ("translationOutputMin", Integer.toString (-IEnvelope.MAX_ENVELOPE_DEPTH / 100)); + bindingElement.setAttribute ("translationOutputMax", Integer.toString (IEnvelope.MAX_ENVELOPE_DEPTH / 100)); + bindingElement.setAttribute ("modBehavior", "add"); + } + + + private static String toLfoShape (final LfoWaveform waveform) + { + // DecentSampler only knows sine, square and saw. The triangle and random waveforms are + // mapped to the closest available shape. + return switch (waveform) + { + case SQUARE -> "square"; + case SAWTOOTH_UP, SAWTOOTH_DOWN -> "saw"; + default -> "sine"; + }; + } + + private static void createPitchModulator (final Document document, final Element modulatorsElement, final IEnvelopeModulator pitchModulator, final int groupIndex) { final double envelopeDepth = pitchModulator.getDepth (); diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/decentsampler/DecentSamplerDetector.java b/src/main/java/de/mossgrabers/convertwithmoss/format/decentsampler/DecentSamplerDetector.java index 89979169..fa9a7bc7 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/decentsampler/DecentSamplerDetector.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/decentsampler/DecentSamplerDetector.java @@ -34,12 +34,16 @@ import de.mossgrabers.convertwithmoss.core.model.IEnvelopeModulator; import de.mossgrabers.convertwithmoss.core.model.IFilter; import de.mossgrabers.convertwithmoss.core.model.IGroup; +import de.mossgrabers.convertwithmoss.core.model.ILfo; +import de.mossgrabers.convertwithmoss.core.model.ILfoModulator; import de.mossgrabers.convertwithmoss.core.model.ISampleData; import de.mossgrabers.convertwithmoss.core.model.ISampleZone; import de.mossgrabers.convertwithmoss.core.model.enumeration.FilterType; +import de.mossgrabers.convertwithmoss.core.model.enumeration.LfoWaveform; import de.mossgrabers.convertwithmoss.core.model.enumeration.PlayLogic; import de.mossgrabers.convertwithmoss.core.model.enumeration.TriggerType; import de.mossgrabers.convertwithmoss.core.model.implementation.DefaultEnvelopeModulator; +import de.mossgrabers.convertwithmoss.core.model.implementation.DefaultLfoModulator; import de.mossgrabers.convertwithmoss.core.model.implementation.DefaultFilter; import de.mossgrabers.convertwithmoss.core.model.implementation.DefaultGroup; import de.mossgrabers.convertwithmoss.core.model.implementation.DefaultSampleLoop; @@ -474,6 +478,7 @@ private void parseGroup (final Element topElement, final IGroup group, final Ele // IMPROVE: Should be added to group itself but needs to be adapted in all other formats final Optional optFilter = parseFilterEffect (topElement, groupElement); final Optional pitchModulation = parsePitchModulation (topElement); + final Optional pitchLfoModulation = parsePitchLfoModulation (topElement); for (final Element sampleElement: XMLUtils.getChildElementsByName (groupElement, DecentSamplerTag.SAMPLE, false)) { @@ -511,6 +516,13 @@ private void parseGroup (final Element topElement, final IGroup group, final Ele pitchModulator.setDepth (envelopeModulator.getDepth ()); pitchModulator.setSource (envelopeModulator.getSource ()); } + if (pitchLfoModulation.isPresent ()) + { + final ILfoModulator lfoModulator = pitchLfoModulation.get (); + final ILfoModulator pitchLfoModulator = sampleZone.getPitchLfoModulator (); + pitchLfoModulator.setDepth (lfoModulator.getDepth ()); + pitchLfoModulator.setSource (lfoModulator.getSource ()); + } group.addSampleZone (sampleZone); } @@ -660,6 +672,48 @@ private static Optional parsePitchModulation (final Element } + private static Optional parsePitchLfoModulation (final Element topElement) + { + // Parse a low frequency oscillator bound to the pitch (vibrato). The oscillator bound to the + // global tuning in the template is a mod-wheel routing and is intentionally not matched. + final Element modulatorsElement = XMLUtils.getChildElementByName (topElement, DecentSamplerTag.MODULATORS); + if (modulatorsElement != null) + for (final Element lfoElement: XMLUtils.getChildElementsByName (modulatorsElement, DecentSamplerTag.LFO)) + { + final Element bindingElement = XMLUtils.getChildElementByName (lfoElement, DecentSamplerTag.BINDING); + if (bindingElement == null || !"GROUP_TUNING".equals (bindingElement.getAttribute ("parameter"))) + continue; + + final double depth = XMLUtils.getDoubleAttribute (lfoElement, DecentSamplerTag.MOD_AMOUNT, 0); + if (depth == 0) + continue; + + final ILfoModulator pitchLfoModulator = new DefaultLfoModulator (depth); + final ILfo pitchLfo = pitchLfoModulator.getSource (); + pitchLfo.setWaveform (toLfoWaveform (lfoElement.getAttribute (DecentSamplerTag.LFO_SHAPE))); + // Only a frequency given in Hertz can be converted; a tempo synchronized rate has no + // representation without a tempo + final String frequencyFormat = lfoElement.getAttribute (DecentSamplerTag.LFO_FREQUENCY_FORMAT); + if (frequencyFormat.isEmpty () || "hz".equals (frequencyFormat)) + pitchLfo.setRate (XMLUtils.getDoubleAttribute (lfoElement, DecentSamplerTag.LFO_FREQUENCY, -1)); + pitchLfo.setDelay (XMLUtils.getDoubleAttribute (lfoElement, DecentSamplerTag.LFO_DELAY_TIME, -1)); + return Optional.of (pitchLfoModulator); + } + return Optional.empty (); + } + + + private static LfoWaveform toLfoWaveform (final String shape) + { + return switch (shape) + { + case "square" -> LfoWaveform.SQUARE; + case "saw" -> LfoWaveform.SAWTOOTH_UP; + default -> LfoWaveform.SINE; + }; + } + + /** * Get the value of a note element. The value can be either an integer MIDI note or a text like * C#5. diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/decentsampler/DecentSamplerTag.java b/src/main/java/de/mossgrabers/convertwithmoss/format/decentsampler/DecentSamplerTag.java index 5e1074ac..f1f9645a 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/decentsampler/DecentSamplerTag.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/decentsampler/DecentSamplerTag.java @@ -31,6 +31,16 @@ public class DecentSamplerTag public static final String MODULATORS = "modulators"; /** The envelope tag. */ public static final String ENVELOPE = "envelope"; + /** The low frequency oscillator tag. */ + public static final String LFO = "lfo"; + /** The LFO shape attribute. */ + public static final String LFO_SHAPE = "shape"; + /** The LFO frequency attribute. */ + public static final String LFO_FREQUENCY = "frequency"; + /** The LFO frequency format attribute. */ + public static final String LFO_FREQUENCY_FORMAT = "frequencyFormat"; + /** The LFO delay time attribute. */ + public static final String LFO_DELAY_TIME = "delayTime"; /** The user interface tag. */ public static final String UI = "ui"; From 6dc57024242524851e6e5bd52adc4c5cc8df4cf3 Mon Sep 17 00:00:00 2001 From: Douglas Carmichael Date: Fri, 24 Jul 2026 07:14:24 -0400 Subject: [PATCH 4/8] Document the pitch LFO (vibrato) support Change log entry plus a note in the SFZ, SoundFont 2 and DecentSampler format sections. --- documentation/CHANGELOG.md | 1 + documentation/README-FORMATS.md | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/documentation/CHANGELOG.md b/documentation/CHANGELOG.md index eea497c3..959f3c99 100644 --- a/documentation/CHANGELOG.md +++ b/documentation/CHANGELOG.md @@ -15,6 +15,7 @@ * New: Added support for envelope time keyboard- and velocity-scaling, which scales the envelope times by the played key and velocity (as opposed to the already supported slopes, which describe the curvature of a segment): Akai S1000, Ensoniq EPS/ASR, Ensoniq Mirage, Logic EXS24, Reason NN-XT, Roland S-7xx, SoundFont 2, Yamaha YSFC. * New: Added support for per-instrument voice settings (polyphony and monophonic legato): Akai S1000, DecentSampler, Disting EX, Ensoniq Mirage, Logic EXS24, Reason NN-XT, Roland S-7xx, SFZ, Synthstrom Deluge, TAL Sampler. * New: Added support for group volume, panning and tuning offsets: Kontakt, DecentSampler, Logic EXS24, Synclavier, TX16Wx, Waldorf Quantum/Iridium. + * New: Added support for a pitch low frequency oscillator (vibrato) with its rate, depth and delay: DecentSampler, SFZ, SoundFont 2. Previously any vibrato was dropped on conversion. * New: Source folders and files are now processed in a stable alphabetical order instead of the file-system enumeration order, so consecutive runs behave identically (and e.g. the QPAT import numbers are assigned in a predictable order). * New: Improved logging if WAV file could not be written. * Fixed: Two filters which differed only in their cutoff envelope were treated as equal, so zones which are not identical could be combined into one; a filter with a cutoff envelope but without a cutoff velocity modulation could additionally throw an exception. diff --git a/documentation/README-FORMATS.md b/documentation/README-FORMATS.md index f177a1bf..727fe625 100644 --- a/documentation/README-FORMATS.md +++ b/documentation/README-FORMATS.md @@ -235,6 +235,8 @@ A dspreset file contains a single preset and the description of the multi-sample There are no metadata fields (category, creator, etc.) specified in the format. Therefore, information is stored and retrieved from Broadcast Audio Extension chunks in the WAV files. If no such chunks are present an [automatic detection](#automatic-metadata-detection) is applied. +A pitch LFO (vibrato) is converted as an `` bound to the group tuning, carrying its rate (in Hertz), depth and delay. The oscillator's fade-in has no equivalent and is dropped, and the waveform is mapped to the nearest of sine, square and saw. + ### Source Options * Create one multi-sample per group: Creates a separate multi-sample for each group instead of one multi-sample which contains all groups. Intended for presets which contain several alternative kits or articulations as groups and switch between them via their user interface (only one group is enabled at a time). Disabled groups are converted as well when this option is enabled; when it is off, only the enabled groups are converted. @@ -609,6 +611,8 @@ The SFZ file contains only the description of the multi-sample. The related samp SFZ can only mark a sample as a one-shot (`loop_mode=one_shot`) if it has no loop. A looped zone therefore keeps its loop and is not written as a one-shot. +A pitch LFO (vibrato) is read and written via the `pitchlfo_freq` (Hertz), `pitchlfo_depth` (cent), `pitchlfo_delay` and `pitchlfo_fade` (seconds) opcodes. + ### Source Options * Log unsupported SFZ opcodes: If enabled, opcodes which are found in the source but are not used (not supported) as input for the conversion are logged. @@ -627,6 +631,8 @@ The conversion process creates one destination file for each preset found in a S There are metadata fields for creator and some description specified in the format. However, additional information like a category is retrieved from Broadcast Audio Extension chunks in the WAV files. If no such chunks are present an [automatic detection](#automatic-metadata-detection) is applied. +The vibrato low frequency oscillator (generators `vibLfoToPitch`, `freqVibLFO` and `delayVibLFO`) is converted to and from the pitch LFO, carrying its depth, rate and delay. The waveform is always a triangle in this format and there is no fade-in. + ### Source Options * Log unused SF2 generators: If enabled, generators which are found in the source but are not used (not supported) as input for the conversion are logged. From a65db9c37f7756652613b02d90bf1185a3abf4bf Mon Sep 17 00:00:00 2001 From: Douglas Carmichael Date: Fri, 24 Jul 2026 07:30:04 -0400 Subject: [PATCH 5/8] DLS: read the vibrato LFO as the pitch LFO 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. --- .../file/dls/DlsArticulation.java | 27 +++++++++++++++++++ .../format/dls/DlsDetector.java | 21 +++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/main/java/de/mossgrabers/convertwithmoss/file/dls/DlsArticulation.java b/src/main/java/de/mossgrabers/convertwithmoss/file/dls/DlsArticulation.java index 2fd3edf6..7f1891f8 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/file/dls/DlsArticulation.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/file/dls/DlsArticulation.java @@ -401,6 +401,33 @@ public static double absoluteTimeToSeconds (final int value) } + /** + * Convert a relative pitch connection value to cent. The value is stored as a 32-bit fixed point + * number with 65536 representing one cent. + * + * @param value The raw 32-bit relative pitch value + * @return The pitch in cent + */ + public static double relativePitchToCents (final int value) + { + return value / 65536.0; + } + + + /** + * Convert an absolute pitch connection value to a frequency in Hertz. The value is stored as a + * 32-bit fixed point number with 65536 representing one cent, where 6900 cent equal 440 Hertz + * (which puts 0 cent at about 8.176 Hertz, the same reference as SoundFont). + * + * @param value The raw 32-bit absolute pitch value + * @return The frequency in Hertz + */ + public static double absolutePitchToHertz (final int value) + { + return 440.0 * Math.pow (2.0, (value / 65536.0 - 6900.0) / 1200.0); + } + + /** * Normalizes a DLS EG Sustain Level lScale value to the range 0.0..1.0. * diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/dls/DlsDetector.java b/src/main/java/de/mossgrabers/convertwithmoss/format/dls/DlsDetector.java index be36f148..4501605d 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/dls/DlsDetector.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/dls/DlsDetector.java @@ -19,6 +19,8 @@ import de.mossgrabers.convertwithmoss.core.model.IEnvelope; import de.mossgrabers.convertwithmoss.core.model.IEnvelopeModulator; import de.mossgrabers.convertwithmoss.core.model.IGroup; +import de.mossgrabers.convertwithmoss.core.model.ILfo; +import de.mossgrabers.convertwithmoss.core.model.ILfoModulator; import de.mossgrabers.convertwithmoss.core.model.IMetadata; import de.mossgrabers.convertwithmoss.core.model.ISampleZone; import de.mossgrabers.convertwithmoss.core.model.implementation.DefaultEnvelope; @@ -242,6 +244,25 @@ private static void applyArticulations (final DlsInstrument dlsInstrument, final pitchEnvelopeModulator.setSource (pitchEnvelope); } + // Pitch LFO (vibrato). The low frequency oscillator which modulates the pitch is the + // vibrato; its frequency and delay are set by their own connections. + final Optional pitchLfoModulation = getArticulation (dlsInstrument, dlsRegion, DlsArticulation.CONN_SRC_LFO, DlsArticulation.CONN_DST_PITCH); + if (pitchLfoModulation.isPresent ()) + { + final double depthCents = DlsArticulation.relativePitchToCents (pitchLfoModulation.get ().getScale ()); + if (depthCents != 0) + { + final ILfoModulator pitchLfoModulator = zone.getPitchLfoModulator (); + pitchLfoModulator.setDepth (Math.clamp (depthCents, -IEnvelope.MAX_ENVELOPE_DEPTH, IEnvelope.MAX_ENVELOPE_DEPTH) / IEnvelope.MAX_ENVELOPE_DEPTH); + + final ILfo pitchLfo = pitchLfoModulator.getSource (); + final Optional lfoFrequency = getArticulation (dlsInstrument, dlsRegion, DlsArticulation.CONN_SRC_NONE, DlsArticulation.CONN_DST_LFO_FREQUENCY); + if (lfoFrequency.isPresent ()) + pitchLfo.setRate (DlsArticulation.absolutePitchToHertz (lfoFrequency.get ().getScale ())); + pitchLfo.setDelay (getTime (dlsInstrument, dlsRegion, DlsArticulation.CONN_DST_LFO_STARTDELAY)); + } + } + // Pitch tuning final Optional pitchTuning = getArticulation (dlsInstrument, dlsRegion, DlsArticulation.CONN_SRC_NONE, DlsArticulation.CONN_DST_PITCH); if (pitchTuning.isPresent ()) From 992f1b64facdea802be97b8d443de72747a1798d Mon Sep 17 00:00:00 2001 From: Douglas Carmichael Date: Fri, 24 Jul 2026 07:33:36 -0400 Subject: [PATCH 6/8] Document the DLS vibrato LFO read --- documentation/CHANGELOG.md | 2 +- documentation/README-FORMATS.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/documentation/CHANGELOG.md b/documentation/CHANGELOG.md index 959f3c99..7b7901ed 100644 --- a/documentation/CHANGELOG.md +++ b/documentation/CHANGELOG.md @@ -15,7 +15,7 @@ * New: Added support for envelope time keyboard- and velocity-scaling, which scales the envelope times by the played key and velocity (as opposed to the already supported slopes, which describe the curvature of a segment): Akai S1000, Ensoniq EPS/ASR, Ensoniq Mirage, Logic EXS24, Reason NN-XT, Roland S-7xx, SoundFont 2, Yamaha YSFC. * New: Added support for per-instrument voice settings (polyphony and monophonic legato): Akai S1000, DecentSampler, Disting EX, Ensoniq Mirage, Logic EXS24, Reason NN-XT, Roland S-7xx, SFZ, Synthstrom Deluge, TAL Sampler. * New: Added support for group volume, panning and tuning offsets: Kontakt, DecentSampler, Logic EXS24, Synclavier, TX16Wx, Waldorf Quantum/Iridium. - * New: Added support for a pitch low frequency oscillator (vibrato) with its rate, depth and delay: DecentSampler, SFZ, SoundFont 2. Previously any vibrato was dropped on conversion. + * New: Added support for a pitch low frequency oscillator (vibrato) with its rate, depth and delay: DecentSampler, DLS, SFZ, SoundFont 2. Previously any vibrato was dropped on conversion. * New: Source folders and files are now processed in a stable alphabetical order instead of the file-system enumeration order, so consecutive runs behave identically (and e.g. the QPAT import numbers are assigned in a predictable order). * New: Improved logging if WAV file could not be written. * Fixed: Two filters which differed only in their cutoff envelope were treated as equal, so zones which are not identical could be combined into one; a filter with a cutoff envelope but without a cutoff velocity modulation could additionally throw an exception. diff --git a/documentation/README-FORMATS.md b/documentation/README-FORMATS.md index 727fe625..eb280902 100644 --- a/documentation/README-FORMATS.md +++ b/documentation/README-FORMATS.md @@ -277,6 +277,8 @@ Both the program (.zbp) as well as the bank (.zbb) are stored as monoliths (zipp The DLS format (*.dls) is a standardized file format developed for storing and distributing collections of digital musical instrument sounds, enabling their use in software synthesizers and hardware devices compatible with the MIDI protocol. It encapsulates audio samples, instrument definitions, articulations, and performance parameters into a single file. Developed in the 1990s initially by the Interactive Audio Special Interest Group (IASIG) and later standardized by the MIDI Manufacturers Association (MMA), with the first formal specification released in 1999. There is no write support. +The amplitude and pitch envelopes, the sample loops and a pitch LFO (vibrato) are read. The vibrato's depth, its frequency (converted from absolute pitch cents to Hertz) and its start delay are carried over to the pitch LFO. + ## Elektron Tonverk The Elektron Tonverk is a dedicated hardware sampler that marks an important milestone for Elektron as its first instrument to support multi-samples. This allows users to map multiple sampled sounds across keys or velocity ranges, creating more expressive and realistic instruments than single-sample playback alone. From bf091f1b8483ed76662ef90a47a1fc917915089c Mon Sep 17 00:00:00 2001 From: Douglas Carmichael Date: Fri, 24 Jul 2026 08:19:56 -0400 Subject: [PATCH 7/8] Add the pitch LFO (vibrato) column to the feature matrix 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. --- .../SupportedFeaturesSampleFormats.ods | Bin 44479 -> 43056 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/documentation/SupportedFeaturesSampleFormats.ods b/documentation/SupportedFeaturesSampleFormats.ods index a60f4bc92e15fa4b2b00f3b73dd7867725e238fa..933ca6a83d41c6c5addbc636eb5cd938707ee54e 100644 GIT binary patch delta 27779 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 delta 29184 zcmZs?Q*hu<@V6V=ww;ZWjcpqn+xAA^*tWId#@g7nZQJ(w{a@5M7w2NSy1J*PYc8hd zsn7J(&VY5VgCQu%fYm5qH)DQnm!g=mOZiVtiMOAp?4=~IOQZL= z!>QWujyO3*H&`GDEm!Y`8osH!@hUd(f zMOPh)t<2x)#oTDFDGSM4RP5fz3s=wY4WDx52*>xWHD#$Bz)&pH_N|!H;0iTt!Wf(& zAH45)7Fxj28ujlADq5vsIQpZ%jU>&+m9+;|V|Tg;o;!WV6c)N%d|YdBixj^}Q~!<` z3q%b^P4D4}ac0eeY}$J1#x;k>h03rjJ4#gC>Z?qspW3EX!w`4+P$-Sh#xuV6`mDOp z!PM053%f_70T=nSkr^jrkcfzja4fBH2%wa1*V6p}Qm7$eUG8C^fnF z@3*M(=5yQ^kO^0MawcEowa7?I8?#L^RH?F<=xckj13|S$uZ3iK93kV)8-;^ke zfPNPZJcr`&P}o7$A@>nGan7I2_9N}oCn3zYp4|leU#$!tQ_`dfD1TvN=YOTlW+HZ_ zn0pE6txdI>&u}cmpfgc%(WXwjYPau3eEs0iSM1)$5T+4^Y(SuuzZ$#!TUO+WpBTVN zIvK^!07RAx8o$+ge6h^Uepa^ALrB~F;Ksx$JcA0Top=TxGf*nag}WccPsSwF@Ck9& z@LAuP4N5kz0(^$v1CsQr_D^?VqCyNs{U z9-3Y`%gC~>#M@A(`iO{}M3JHWMBvo8OSA384J>f>L56fzP=Rtz^f@z?ga~D942k?{ z>#>4OxBYD=Y-XvbRGmSE{m_}sG!lX*tMqTR01;tO4AIaMIxuKb@Po@v;anI}Z=;&6?L?E-h(uikJh9D?$z(p_^=$4B=^-=GY-&7s0;a zAs`)@<1u;sH_CK5G=BGj&|j}Mc!($eF}}I9&ep8=mm^ERRZQ@&K11&(3a+>*iw3-i zLl{*j>Se-@-`b12y;cDqQ#2%*XH}Zwa&ydMHKi3fC=#~vNdx6@OpHMlQjh~gsbF5N z#EHa}nGBJuTS2-s;Wzfhr(-urdpYFRuE2pg%FiABG2CVwx$rRoQaod4xnq$&r;O<6pyckuRBIgUo&Y9Da{tN(QOL~^X&!1amnZO19dq!MkW_^833tG~t>%wz5Jyd!7 ztNCzqW+vyNe#Ec_)+oBfyQKe4DESRH=DgArI!qh3$Xez-g`8=(ri%aRH>xQYFHjdT zPUJDVzxYV_%XLvJl52oit~7R&$r6x|)&S|cmVVN~_`q_nO`+s*cB_^c79-NIrKG8_ z)^n+vs@{c$4keS#YTk{%)Dhv-F=G28%x;du5O2&KdAOMleJG)gEptxSPfr!h=ha9H z`zxy2sk#YYq&Q!6Vm9z^{P{Vke1bMI3~_xJ>@Y^l*if6z@O(qT=?XQn<{zNn`?bqg zX}H8Z+I`q3`RnapnVexICwk2L&~m$d225w$v^3vQ zk1?|jMr!+zGZSw!kJCs>_iDh4v#}#3w&mlx`+MKqR<8bngaG`wk!e;<^WP)2f8G&U z=asmh(TUBCN^(0<#-N%*kb_b^xdCv$DBaY!2XR* zxIX&7{a%%ait6nwredfyx0rWHI98$fQGccn9>!uuCNCYS=-!!xqX5Y>h(dJlI&ji) z@AzVr_TZ26mAA94t08~uI;{$KCR&==Z4%&=Wq&z0SJGGb&czCkHlJOA4rd^S--0lZ zgk5#u4O4vFW#j8})KXDPRky8e!*GiA+lO}vaf*rBpHM6*lKMIp*YSZuS|}E< zDlF7l+#S%xcVAQvrp{jw5@>XAU_RlnQ4v|3S@1OqvQX8(_j%*7xT2uLSB)#vI`S8T znfE`+sSRcET^6^iaS-g=91&`j64Ur}S&n;2>GIJ^D3vbB{{SAI%$uVIvp0(8h>-r2 zcU4Ign399mX*`)OS(wKDQBWS0r{=z2zeS01K*i`O?+2Cb z7WG!HSnIO618Toq;d*b_&M!E+TWx-T99)_%NV?H~DQ{z)b!?0VA5 zdCvYGnV2ShO8g~A4RR`SrTSdz^Mn;*M7qA4f)9jK?iJH}BLp`J<-D+&uXuDPM} zNF`6_rElXa!QRH|OT7%jPO8PYI&4&VYD-Z?xlE9E!s!ZZhUuDd^Lrh8uT7P>GASr49-p0 z5b`b!ytEHgx{0)k*^1%eVgH1E*DE+ZHzvxe%sKaihyd*@AvIw~373D!>h+WTRb4l> z%o5mNL64F2D*da4YwJTsGJLU>cGrh;iLK3ITWzHwz{q5nC*8yN?iMjt+4G}Yt-=5y zh%*XS=To7UhKM?(KkWD#nxW&ldHVNjdgA%ByO*ex@@|QvK_YXIbBQ(=Clc$T|J6x1 zUg9C!qwoF3NB0U^VQboSo*;0<2|X#b

CYoBy}mzskq1ULG;1b5IS$KWq<(1%YxC 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 Date: Fri, 24 Jul 2026 15:44:54 -0400 Subject: [PATCH 8/8] DLS: only read a vibrato which is not controlled by a wheel 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. --- documentation/README-FORMATS.md | 2 +- .../file/dls/DlsArticulation.java | 12 +++++++ .../format/dls/DlsDetector.java | 32 ++++++++++++++++++- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/documentation/README-FORMATS.md b/documentation/README-FORMATS.md index eb280902..717147d2 100644 --- a/documentation/README-FORMATS.md +++ b/documentation/README-FORMATS.md @@ -277,7 +277,7 @@ Both the program (.zbp) as well as the bank (.zbb) are stored as monoliths (zipp The DLS format (*.dls) is a standardized file format developed for storing and distributing collections of digital musical instrument sounds, enabling their use in software synthesizers and hardware devices compatible with the MIDI protocol. It encapsulates audio samples, instrument definitions, articulations, and performance parameters into a single file. Developed in the 1990s initially by the Interactive Audio Special Interest Group (IASIG) and later standardized by the MIDI Manufacturers Association (MMA), with the first formal specification released in 1999. There is no write support. -The amplitude and pitch envelopes, the sample loops and a pitch LFO (vibrato) are read. The vibrato's depth, its frequency (converted from absolute pitch cents to Hertz) and its start delay are carried over to the pitch LFO. +The amplitude and pitch envelopes, the sample loops and a pitch LFO (vibrato) are read. The vibrato's depth, its frequency (converted from absolute pitch cents to Hertz) and its start delay are carried over to the pitch LFO. Only a connection which is not modulated by a controller is read as the vibrato; the format normally contains a second one controlled by the modulation wheel, which is the amount the wheel can dial in and would sound permanently if it were converted. ## Elektron Tonverk diff --git a/src/main/java/de/mossgrabers/convertwithmoss/file/dls/DlsArticulation.java b/src/main/java/de/mossgrabers/convertwithmoss/file/dls/DlsArticulation.java index 7f1891f8..c42f47e2 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/file/dls/DlsArticulation.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/file/dls/DlsArticulation.java @@ -366,6 +366,18 @@ public int getSource () } + /** + * Get the controller which modulates the connection, e.g. the modulation wheel. A value of + * CONN_SRC_NONE means that the connection is always fully applied. + * + * @return The control, see the CONN_SRC_* constants + */ + public int getControl () + { + return this.control; + } + + /** * Get the destination. * diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/dls/DlsDetector.java b/src/main/java/de/mossgrabers/convertwithmoss/format/dls/DlsDetector.java index 4501605d..6e38334d 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/dls/DlsDetector.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/dls/DlsDetector.java @@ -246,7 +246,10 @@ private static void applyArticulations (final DlsInstrument dlsInstrument, final // Pitch LFO (vibrato). The low frequency oscillator which modulates the pitch is the // vibrato; its frequency and delay are set by their own connections. - final Optional pitchLfoModulation = getArticulation (dlsInstrument, dlsRegion, DlsArticulation.CONN_SRC_LFO, DlsArticulation.CONN_DST_PITCH); + // Only an *uncontrolled* connection is read: the format additionally defines the same + // connection controlled by the modulation wheel, which is the amount the wheel can dial in + // and must not be applied as a permanently sounding vibrato. + final Optional pitchLfoModulation = getUncontrolledArticulation (dlsInstrument, dlsRegion, DlsArticulation.CONN_SRC_LFO, DlsArticulation.CONN_DST_PITCH); if (pitchLfoModulation.isPresent ()) { final double depthCents = DlsArticulation.relativePitchToCents (pitchLfoModulation.get ().getScale ()); @@ -285,6 +288,33 @@ private static Optional getArticulation (final DlsInstrument dl } + /** + * Get a connection which is not modulated by a controller, e.g. the modulation wheel. + * + * @param dlsInstrument The instrument + * @param dlsRegion The region + * @param source The source, see the CONN_SRC_* constants + * @param destination The destination, see the CONN_DST_* constants + * @return The connection, if any + */ + private static Optional getUncontrolledArticulation (final DlsInstrument dlsInstrument, final DlsRegion dlsRegion, final int source, final int destination) + { + final Optional articulation = getUncontrolledArticulation (dlsRegion.getArticulations (), source, destination); + if (articulation.isPresent ()) + return articulation; + return getUncontrolledArticulation (dlsInstrument.getArticulations (), source, destination); + } + + + private static Optional getUncontrolledArticulation (final List articulations, final int source, final int destination) + { + for (final DlsArticulation articulation: articulations) + if (articulation.getSource () == source && articulation.getDestination () == destination && articulation.getControl () == DlsArticulation.CONN_SRC_NONE) + return Optional.of (articulation); + return Optional.empty (); + } + + private static Optional getArticulation (final List articulations, final int source, final int destination) { for (final DlsArticulation articulation: articulations)